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>
Fix 1: mem_boot_count_inc prunes all existing soul:boot_count nodes before
inserting the new one — keeps exactly one boot counter node instead
of accumulating a new node per boot. Also fixes a latent ordering
bug where engram_search_json oldest-first results caused the counter
to read stale (low) values once >3 copies accumulated.
Fix 3: handle_api_node_delete comment clarified — the no-verify exception
is correct for deletes (not a write path); read-back-verify is for
writes only.
Fix 4: emit_session_start_event prunes old session-start InternalStateEvent
nodes after each boot, keeping the 10 most recent and forgetting
older ones. Prevents unbounded accumulation of ~120+ copies.
- PR #60: inject operator home dir into system prompt (#30)
Adds OPERATOR IDENTITY section so the LLM correctly resolves
'my files/notes/desktop' to the actual running user's $HOME.
Prevents identity confusion between imprint author and operator.
- PR #61: plan-mode endpoint POST /api/chat {mode:'plan'} (#27)
Adds handle_chat_plan — returns {steps:[{id,title,detail}]} JSON.
Wired into all three /api/chat route handlers. Grounds the plan
via engram_compile (same as agentic path) for context awareness.
dist changes:
- soul.c: both PRs compiled in; build_system_prompt updated to
2-param signature (ctx, chat_mode); handle_chat_plan added
- chat.c/routes.c/chat.elh: individual module outputs updated
- elp-c-decls.h: remove stale 1-param build_system_prompt decl,
add handle_chat_plan declaration
- soul.elh.c: new soul header declarations file (from PR #60)
Compile verified: cc -O2 -DHAVE_CURL soul.c el_runtime.c -lcurl
Binary: 805K arm64, smoke test passes (port in use = expected).
Both ci.yaml and deploy-gke.yaml triggered on push/main and shared the
neuron-runner concurrency group. Gitea's cancel-in-progress:false protects
running jobs but not queued ones — a new push arriving while a build was
in progress cancelled the queued deploy job from the previous push, leaving
the soul permanently at 0/0 replicas on GKE.
Fix: add deploy as a needs:build job in ci.yaml so build+deploy are a single
workflow instance. One push queues one instance — no more orphaned deploys.
deploy-gke.yaml is demoted to workflow_dispatch-only for manual slot overrides.
elb runs elc which consumes 24GB+ virtual memory on the 16GB GCE runner,
OOM-killing the runner process and crashing the VM. We already restore the
repo's pre-built soul.c immediately after elb runs, so elb's output is
discarded anyway. Skip elb entirely: download only the El runtime headers
and compile dist/soul.c directly.
Root cause: runner VM was unresponsive for 7+ weeks due to repeated elc
OOM kills. VM was manually reset 2026-06-28 to restore CI.
Merge pull request fix(mcp-wrapper): planWork creates a real BacklogItem; reviewBacklog lists by type (#59) from fix/wrapper-backlog-endpoints into main
planWork fell through create_typed_node to a generic /api/neuron/memory write — a [BacklogItem]-prefixed
memory blob with title/project/priority DROPPED, never a real BacklogItem. reviewBacklog used a lexical
/recall (top-50, untyped). Now: planWork -> /api/neuron/node/create {node_type:BacklogItem,...} via new
create_node_typed; reviewBacklog -> list_typed('BacklogItem') (GET /api/neuron/list/BacklogItem). elc-clean.
Depends on neuron PR #58 (the list/<type> slice fix) to round-trip; needs the wrapper binary rebuilt +
:7779 restarted to take effect.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
str_slice(clean, 16, ...) left a leading slash on node_type ('/BacklogItem'), so
engram_scan_nodes_by_type_json matched nothing and list/<type> returned [] for EVERY type — silently
breaking backlog + typed-node listing across the app and MCP tools (reviewBacklog). Proven live: the
literal-scan endpoint /api/neuron/knowledge returns nodes; /api/neuron/list/Knowledge returned []. elc-clean.
NOTE: soul-core — needs dist/soul.c regen (Will); rides the same rebuild as #56/#57.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds /api/connectors/call -> connectd /mcp/call, so the app can invoke a connector tool (e.g. WhatsApp
get_pairing_qr / get_login_status for the pairing UI) through the soul, keeping app->soul->connectd
intact (UI never hits connectd directly) and working for future remote/hosted clients. elc-clean.
NOTE: soul-core change — needs dist/soul.c regen (Will), can ride the same rebuild as PR #56.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
handle_chat_agentic now reads body image + image_media_type and, when present, sends the current
user turn as an Anthropic content-block array [{text},{image}] instead of a plain string — so the
model sees raw pixels alongside memory, history, and tools (parity with the CLI). Additive: no image
=> output byte-identical to before. elc-clean. Pairs with neuron-ui fix/chat-vision-attachments.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
neuron.c and routes.c were compiled against the old 1-arg soul interface.
chat.c already uses the 2-arg signature. The Windows cross-compile build
generates elp-c-decls.h from all dist/*.c files, causing a conflicting-types
error when both signatures appear. Recompile these modules against the
current soul API to eliminate the conflict.
Linked against dev runtime with is_knowledge fix that adds Knowledge
node type. Engram goal_bias now gives Knowledge nodes +0.3 boost on
technical queries, consistent with how Belief/DharmaSelf/Safety nodes
are already treated. Same el_runtime source as concurrent foundation/el
commit 16d62bd.
All 8 session_preload node accesses (3 profile, 2 work, 2 project, 1
summary) now check id_in_seen(node_id, seen_ids) before including
content. seen_ids is populated by engram_compile via state and covers
all nodes already in the activation+search context block. Prevents
high-salience nodes from appearing twice in the system prompt.
Issue 1 (CRITICAL): Restore parse_float_x100 for correct single-decimal
float handling. "0.9" now correctly yields 90, not 9. Also restores
engram_numeric_valid guard that validates inputs before str_to_int.
Issue 2 (CRITICAL): Fix handle_chat_agentic safety screen history key
regression. state_get("conversation_history") -> state_get("conv_history")
so the safety screen receives actual history instead of always "".
Issue 3 (REAL BUG): Replace _sel_N JSON sentinel injection in
engram_compile_ranked with |N| index string tracking. Sentinels were
leaking into node JSON delivered to the LLM and cleanup only covered
indices 0-14, leaving indices 15+ uncleaned.
Issue 4 (REGRESSION): Restore rendered conversation history formatting.
Conversation history is now rendered as "User: .../Assistant: ..." with
400-char truncation per turn, not raw JSON array injection.
Issue 5 (SCOPE/SAFETY): Restore removed defensive code: engram_numeric_valid
and parse_float_x100 guards; conv_history_load label-based fetch + partial-
write guard + load-failure state flag; conv_history_persist partial-write
guard + failure logging; hist_warning in response envelope.
Issue 6 (UNDOCUMENTED): Restore bell event cutoff from 259200s (3 days)
back to 1209600s (14 days). Also restore PositiveEvent affective context
search that was removed alongside the cutoff change.
Issue 7 (LOGIC REGRESSION): Fix affective_prefix to run every turn
(not just hist_len == 0). The care/joy directives must persist throughout
the session, not vanish after turn 1.
Issue 8 (MINOR): session_summary_write_dated now uses el_from_float(0.85)
for salience and importance (two-decimal) to avoid any ambiguity in float
parsing, and the function is re-added with the session-end hook.
Two design bugs in the state_set placement caused the dedup seen-ID set
to be incomplete even with callsites wired up:
1. state_set("engram_compile_seen_ids") was called immediately after
merging the main node pools, before scan_part (persona fallback) and
affective_part (bell node) were computed. Nodes appearing only in
those segments were never added to the seen set.
2. affective_part is a bare JSON object (bn0 from json_array_get), not
a JSON array. Passing it to engram_extract_ids would have gotten
json_array_len == 0 and silently skipped the affective node's ID.
Fix: move state_set to after ctx is assembled from all three segments.
Extract ids_from_merged and ids_from_scan via engram_extract_ids (both
are JSON arrays), and extract ids_from_affective via json_get(affective_part, "id")
directly since it is a bare object. Merge all three via add_to_seen
before publishing to state.
Thread a seen-node-ID exclusion set from engram_compile() through to
session_preload in handle_chat, preventing the same high-salience nodes
(identity, recent memories) from appearing 2-3x in the system prompt.
Changes:
- Add id_in_seen(), add_to_seen(), engram_extract_ids() helpers that
maintain a comma-delimited seen-ID accumulator (EL has no Set type)
- In engram_compile(): after merging all topic/entity/recall pools, extract
node IDs from merged_nodes and publish via state_set(engram_compile_seen_ids)
- In handle_chat(): read seen_ids from state after engram_compile() returns,
then check id_in_seen() before emitting each session_preload bullet
(profile x3, work x2, project x2, summary x1 — all 8 candidate nodes guarded)
Nodes already present in the compiled engram context are skipped in preload,
eliminating 3000-3500 token repetition on first-message turns.
engram_compile() already published seen node IDs to state via engram_compile_seen_ids
but handle_chat never read or applied them. Wire up the consumption side:
- Read engram_compile_seen_ids from state after engram_compile() returns
- Check each session_preload candidate node (profile x3, work x2, project x2,
summary x3) against id_in_seen() before emitting its content bullet
- Nodes already present in the compiled engram context are skipped entirely,
preventing the same high-salience identity/memory nodes from appearing 2-3x
in the system prompt and burning 3000-3500 tokens on repetition
- Add engram_render_node/render_nodes/dedup_nodes helpers for human-readable
prose bullet output instead of raw JSON node objects reaching the LLM
- Fix engram_compile_ranked to use |N| index sentinel instead of _sel_N JSON
mutation which leaked sentinel fields into LLM-visible node data (Issue #11)
- Update build_system_prompt with chat_mode param; no_tools_rule only included
for chat path, not agentic paths (Issue #9)
- Move engram block to end of system prompt for strongest LLM attention (Issue #8)
- Label sections: STABLE IDENTITY vs RETRIEVED MEMORY (Issue #10)
- Render conversation history as User:/Assistant: dialogue instead of raw JSON
- Add RETRIEVED MEMORY labels to agentic and dharma room system prompt assembly
- Cache bell node in engram_compile state (engram_compile_bell_node)
so handle_chat reads cached value instead of duplicate bell query (Issue 2)
- Cache activation result (engram_compile_activation_json) for strengthen_chat_nodes
reuse — eliminates third activation query per turn (Issue 7)
- Fix context cap to truncate at clean JSON object boundary (Issue 6)
- Cache bell node result in engram_compile state (engram_compile_bell_node)
so handle_chat affective_prefix reads the cached value instead of firing
a duplicate engram query for distress signals (Issue 2)
- Cache primary activation result in engram_compile state
(engram_compile_activation_json) using nodes0 from engram_compile_multi
- Replace redundant engram_activate_json(message, 2) in strengthen_chat_nodes
with state_get(engram_compile_activation_json) - eliminates a third
activation query per turn (Issue 7)
- engram_compile already has object-boundary truncation and cross-set
dedup via engram_nodes_merge/engram_dedup_nodes (Issues 1, 6, 9)
- Issue 2: replace raw 50-char threshold with is_genuine_continuation() that
checks for explicit follow-up phrases and mid-sentence capitalization (proper
nouns signal a new topic, not a continuation)
- Issue 3/8: build_activation_seed() scans back to find the prior USER turn as
the topic anchor instead of using the last assistant reply (hist_len-1)
- Issue 4: engram_compile_multi() fans out across three seeds — enriched primary,
raw message (entity queries), and emotion query — merging non-redundant results
- Issue 5: agent workspace_root appended to ag_seed so agentic activation is
workspace-aware; previously ignored despite being available in state
- Issue 6: distill_transcript() extracts salient tail+question content from full
transcripts before passing to engram_compile in dharma room handlers
- Issue 7: dist/soul-with-nlg.el handle_chat and handle_chat_agentic now load
history and use build_activation_seed() — the raw message path is eliminated
- Issue 9: topic_snip_from_entry() takes the TAIL 200 chars of a long reply and
finds the last sentence boundary — captures end-of-reply named concepts
- Issue 10: multi_turn_topic() pulls up to 3 prior user turns into the non-
continuation seed so earlier thread context re-activates high-salience nodes
Fix critical float parsing bug: %g serializes 0.70 as '0.7', naive str_replace
dot-strip gives str_to_int('07')=7 not 70. New parse_salience_100() uses
str_index_of to detect single-decimal strings and multiplies by 10 to correct.
Affects conv nodes (0.6/0.7), default memories (0.5/0.5), utterance nodes (0.6)
— the majority of the graph was scoring near zero and filtered by threshold=25.
Fix recency to use max(created_at, updated_at, last_activated) so nodes
strengthened by engram_strengthen() after chat turns score as fresh, not by
original write time. A node referenced yesterday but created 25 days ago
was borderline-filtered; now correctly scores fresh.
Compress recency dynamic range from 10x (10-100) to 1.54x (65-100) via
formula (50 + recency/2). Old formula: sal*imp*recency/10000 let recency
dominate — a canonical high-importance node at 30 days scored identical to
a fresh noise node. New: high-importance nodes remain competitive when old.
Add tier-aware decay with softer floor (30 not 10): Canonical nodes decay
over 365 days, Episodic over 90 days, working/untiered over 35 days. Long-
term identity and persona nodes are no longer permanently filtered.
Lower threshold from 25 to 15 to admit moderately-relevant older nodes that
pass scoring with the corrected formula. Backfills recall coverage lost when
single-decimal nodes were being silently discarded.
Apply scoring to activation nodes: engram_compile_ranked(activate_json, 5)
replaces unconditional pass-through. Threshold 5 preserves recall while
excluding genuinely zero-quality stale nodes.
Extend sentinel cleanup in engram_compile_ranked from _sel_0-9 to _sel_0-19
so max_nodes can safely be increased past 10 without JSON corruption.
Fix critical float parsing bug in engram_score_node: str_replace('.','')
then str_to_int silently miscored single-decimal salience strings (0.9->9,
0.7->7, 1.0->1). Introduce parse_salience_100() which detects decimal
position and scales correctly (no decimal: *100; one decimal: *10;
two decimals: as-is).
Replace flat 30-day linear decay with tier-aware decay curves: Canonical
nodes use a 365-day window (foundational identity resists aging), Episodic
nodes use 90 days, Working/untiered keep the existing 30-day slope. Floor
stays at 10 for all tiers.
Use max(created_at, updated_at) as the recency reference so revised nodes
are not penalised for their original creation date.
Extend affective context windows from 72h/7d to 14 days across all three
paths (engram_compile, handle_chat, soul.el load_identity_context) so a
Friday crisis carries into Monday sessions and all paths present consistent
context. The 72h/7d split caused conflicting affective context between
soul.el (which loaded a 5-day-old crisis node) and chat.el (which excluded
it on subsequent turns).
Add salience evolution to mem_consolidate: strengthen top working-memory
nodes (recently recalled across sessions) and Canonical-tier nodes
(foundational identity must not decay to the floor). Previously consolidate
returned structural counts only with no salience changes.
Expand conversation window from 20 to 40 turns in both handle_chat and the
agentic history trim. Long technical sessions were losing early problem
framing at 10 user + 10 assistant pairs.
- Q1: engram_numeric_valid() guard against non-numeric timestamps in bell scoring
- Q2: soul-agnostic cold-start fallback in engram_compile (drops genesis-specific hardcoded node IDs)
- Q3: partial-write guard and failure logging in conv_history_persist/load
- Q4: document circuit-breaker limitation requiring C runtime support
- Q5: println warnings on empty activation/search paths
- Q6: load_identity_context warns when all identity fetches return empty
- Q7: recall_status state tracking (ok/empty/unavailable) surfaced to LLM via MEMORY STATUS block
- Q8: document shared-state race conditions in engram_recall_status and safety_system_addendum
- CRITICAL BUG: conv_node_id empty check moved outside is_bell block so silent Conversation node loss is always logged
Merge propose/agent-workspace-root-read (Tim's PR #28):
- path_within_root now appends '/' to root before prefix check (closes proj_evil bypass)
- edit_file in dispatch_tool now checks agent_workspace_root() and resolves path
- handle_chat_agentic reads agent_workspace_root from request body, only persists if non-empty
- Safety screen preserved after workspace root read (conflict resolved)
Merge improve/safety-crisis-detection (PR #31): reads layered_cycle_safety_system_addendum
from state and appends to system prompt on each turn (cleared after use to prevent bleed).
Safety ts extraction falls back to updated_at. Affective prefix now wires into system build.
Conflict with PR #33 resolved: capability_rules and session_preload both preserved.
- engram_compile: BellEvent nodes do not carry created_at in the engram
node JSON; extract the unix timestamp from the embedded ' | ts:NNNNN'
pattern in the content string instead. Fall back to created_at/updated_at
if the marker is absent. Guard str_to_int against empty string so the 72h
recency check never silently treats every node as epoch-0 stale.
- auto_persist: append the current unix timestamp to the BellEvent label
('bell:soft:1749876543') to make it unique per turn. The previous label
('bell:soft') was the same for every soft bell, causing engram to treat
all subsequent writes as updates to the same node.
- Remove dead soft_bell block in layered_cycle that wrote soul_safety_system_augment
to state but was never read; safety augmentation now goes through the correct
layered_cycle_safety_system_addendum state key read by build_system_prompt
- build_system_prompt now reads layered_cycle_safety_system_addendum and appends
it to the system prompt, clearing the key after consumption
- Timestamp extraction for distress nodes falls back to updated_at when created_at
is empty, preventing the 72h recency check from always treating nodes as stale
- Issue #3: err_404/err_405 now emit HTTP 404/405 via __status__ envelope instead of HTTP 200
- Issue #4: add auth_check() function to handle_request; enforces NEURON_TOKEN on all routes except /health and /lineage
- Issue #5: missing required params now return HTTP 400 (__status__ envelope) in /api/chat (GET+POST), /imprint/contextual, /imprint/user, and handle_chat
- Issue #6: LLM unavailable in handle_chat now returns HTTP 503 instead of HTTP 200
- Issue #7: add 32 KB message size guard on POST /api/chat before engram_compile and LLM
- Issue #8: add TODO comment to route_health documenting the live-engram-query problem and the /health/deep split plan
- Issue #9: add comment to hist_trim documenting fragile str_index_of parser and silent data corruption risk
- Issue #10: add TODO comment in handle_request documenting missing per-IP rate limiting
- Issue #11: fix connectd_post temp file collision — add monotonic sequence counter so concurrent requests get unique paths
- Issue #12: fix call_mcp_bridge fixed temp file race — add monotonic sequence counter for unique paths under concurrent load
- Issues #1/#2: add TODO comment in handle_request documenting EL no-exception limitation and SIGSEGV handler gap
Issue #5: detect empty string from llm_extract_text() as an error in handle_chat,
handle_chat_as_soul, and handle_dharma_room_turn. The C runtime silently returns ""
when the LLM response content array is missing or all blocks fail to parse; without
this guard the empty string passes through to callers as a silent empty reply.
Issue #9: make agentic_loop max_tokens configurable via NEURON_LLM_MAX_TOKENS env
var (default 4096). The hardcoded value is marginal for long tool chains (8 iterations
x 4096 tokens); operators can now set 8192+ for complex multi-step tasks without
rebuilding. Non-agentic path (llm_call_system) still uses the C runtime hardcode —
that fix lives in el_runtime.c (see TODO block added in this commit).
Issue #10: increase connector_tools_json and tool_auto_approved curl --max-time from
2s to 5s to reduce false-empty tool lists when neuron-connectd is under transient
load. Graceful degradation to [] on bridge down is unchanged.
Issues #1/#2/#3/#4/#6/#8: documented as TODO comments in chat.el. These require
targeted C runtime changes in el_runtime.c (llm_provider_request retry loop,
EL_LLM_TIMEOUT_MS separation, HTTP 429 backoff, 5xx retry, EL_HTTP_MAX_RESPONSE_BYTES
cap). Architectural decisions recorded so they are traceable to root causes.
- sessions.el: add session_exists() for chat-path session validation (ISSUE #6/#7)
- sessions.el: add session_create_cleanup() for ghost-session rollback (ISSUE #1)
- sessions.el: set session_pending_first_msg flag in session_create; clear it in
session_hist_save so the first successful chat marks the session active (ISSUE #1)
- sessions.el: session_delete now clears mcp_bridge:<id> and always_allow_<id>
state keys so abandoned pending-tool sessions do not accumulate (ISSUE #5)
- sessions.el: add TODO comments for ISSUE #2 (no TTL/expiry), ISSUE #3
(non-atomic delete-then-create), ISSUE #4 (no concurrent-create guard),
and ISSUE #8 (reconnect/duplicate resume race) where fixes are too invasive
to land without new runtime primitives
- chat.el: validate session_id exists via session_exists() before entering
agentic_loop; unknown session_ids now return a 404-style error instead of
silently starting a fresh empty session (ISSUE #6/#7)
Every engram_node_full call that dropped its return value now binds it
and emits a println on empty string. engram_save calls in consolidate,
heartbeat, and dharma-room-turn are checked for failure. The two API
handlers (log_state_event, tune_config) that skipped api_persisted()
now match the read-back-after-write contract used everywhere else in
neuron-api.el.
Files changed:
- chat.el: conv_history_persist, handle_dharma_room_turn, auto_persist
- soul.el: emit_session_start_event, seed_persona_from_env HTTP check
- memory.el: mem_save, mem_boot_count_inc
- neuron-api.el: handle_api_log_state_event, handle_api_tune_config,
handle_api_consolidate (engram_save + session summary write)
- awareness.el: ise_post local-engram fallback path
TODO comments added for non-atomic patterns (issues #12, #13) and
the missing circuit breaker (#14) — these require new primitives.
- Fix state key mismatch: soul.el layered_cycle now reads conv_history
(not conversation_history), unblocking the safety_score_distress_history
history-amplification path in safety_threat_score
- Add safety_augment_system call on the main handle_chat path so the
phrase-list bell detector fires on all chat turns, not just dharma rooms
- Add cross-session affective engram query in load_identity_context() at
boot; stores distress/crisis signals from prior sessions under
soul_affective_context with a 7-day soft recency filter
Issues addressed:
- #1 ASYMMETRIC PERSIST/LOAD: conv_history_load() now tries engram_get_node_by_label()
first (symmetric with the label-based write), falling back to vector search only when
label lookup returns nothing. Immune to cold/corrupt vector index.
- #2 SILENT LOAD FAILURE: all failure paths in conv_history_load() and conv_history_persist()
now emit a println log line rather than silently returning "" or dropping writes.
- #3 NO RECOVERY PATH: documented as TODO with explanation of why a full recovery path
(retry, ID fallback, orphan cleanup) is too invasive for a targeted fix here.
- #4 OVERWRITE WITHOUT DELETE: documented with TODO to replace engram_node_full with
explicit delete-then-create once engram exposes a label-scoped delete API.
- #5/#10 BROKEN TRIM / OFF-BY-ONE: hist_trim() rewritten to use json_array_len /
json_array_get (structural JSON ops) instead of raw str_index_of scanning for
'{"role":' markers. Immune to marker strings appearing inside message content.
Minimum retained count guard added: never trims below 2 entries.
- #6 PARTIAL-WRITE GUARD: conv_history_persist() refuses to write a blob that doesn't
contain both '[' and ']'. conv_history_load() requires both before accepting content.
- #7 DUAL STORAGE: documented with a comment at the persist call site.
- #8 NO MAX SIZE GUARD: documented as TODO with rationale for why a byte-length cap
requires a more invasive change (entry truncation or summarisation).
- #9 AGENTIC HISTORY NOT PERSISTED: handle_chat_agentic() now calls conv_history_persist()
for the default global session (hist_key == "conv_history") after updating state,
matching the non-agentic path's durability. Named sessions remain in-process only.
Add strip -s after gcc compilation to remove symbol table and relocation info.
Reduces binary size and prevents symbol-level reverse engineering of EL runtime internals.
- auto_persist: detect bell level (soft/hard) on every user message using
safety_detect_bell_level; write a dedicated BellEvent engram node with
calibrated salience alongside the Conversation node when a bell fires.
Tag the Conversation node with bell:soft/bell:hard and 'affective' for
direct discovery without scanning all chat nodes.
- auto_persist: track per-session bell count, dominant level, and last
signal in state (session_bell_count/level/signal keys) so downstream
functions can act on the emotional history without re-scanning engram.
- engram_compile: include the top-1 most recent BellEvent node within 72h
in every context build. Distress context from earlier turns (same or
recent session) automatically travels into all subsequent LLM calls.
- hist_trim_with_bell_guard: replace hist_trim at the handle_chat call site.
Before evicting the oldest turn from the 20-turn window, inspect the user
message for bell signals. If a bell was present, write a preservation
BellEvent to engram before dropping the turn so the full message survives
the rolling window.
- session_hist_save: after writing the history node, check session bell
counters. On the first save where bell_count > 0, write a
session:emotional-summary BellEvent node with distress signal, count,
and dominant level. A state flag prevents duplicate writes on subsequent
saves in the same session.
- engram_compile: rank search results by recency x relevance before including
in context. Pulls 20 candidates, scores each (salience * importance * recency
decay), keeps top 8. Eliminates stale/low-signal nodes that diluted context.
- handle_chat: on hist_len==0 (session start), proactively load user profile
and active-work context from engram and inject as brief bullets in the system
prompt. Gives the soul grounding before any conversation history exists.
- build_system_prompt: add [CAPABILITY GAPS] directive instructing the soul to
offer partial help and reasoning instead of flat "I don't have access to that"
refusals when a tool is missing.
- handle_chat_agentic: run safety_screen at entry, mirroring layered_cycle.
Hard bell exits immediately with the crisis response without entering the loop.
- agentic_loop: surface the 8-iteration cap explicitly in the error envelope
("agentic loop hit the 8-iteration cap...") rather than the opaque "no response".
Add iterations count to both the error and success envelopes for observability.
- Add per-IP in-memory rate limiter (60 req/min default, configurable via
soul_rate_limit state key; /health exempt; loopback callers skipped)
- Extend /health with uptime_secs (from soul_boot_ts) and live LLM probe
- Add missing_param 400 guard on POST /api/chat before passing to LLM
- Standardise error envelopes: add "code" field to err_404/err_405 and all
missing-param returns; route_synthesize now errors clearly instead of
returning the misleading {"mechanism":"did not engage"} on bad input
- Document streaming gap in /api/chat (SSE not implemented, note added)
- handle_request gains ip param; rate_limit_check wired at entry point
- soul.el: fix state key bug in layered_cycle (conversation_history -> conv_history)
- safety.el: add indirect crisis location patterns to soft_bell phrase list
- soul.el: wire safety_augment_system into layered_cycle for soft_bell turns
- chat.el: load cross-session affective context at session start when distress signals found within 72h
REPRODUCED: in the non-agentic path (Tools off / 'Just chat'), asking for
tool-work makes the model role-play tool use — it emits a fake ```json {...}```
'tool call' and says 'let me search/query/pull your sessions' while NOTHING
runs. Reads as a broken/lying app. (The agentic path is fine: verified it
calls search_memory and reports honestly.)
Root cause: build_system_prompt (handle_chat, the tool-less path) never told
the model it has no tools this turn, so it fabricated.
Fix: add a NO-TOOLS directive to the non-agentic system prompt — never emit
tool calls / JSON tool blocks / 'let me pull...' narration; answer from context
only; if a tool is truly needed, say so in one sentence and tell the user to
turn Tools on. Applied to chat.el (source) AND dist/soul.c (the curated TU the
CI compiles), so the CI-built binary carries it.
Verified the FABRICATION repro on the live local soul; could not verify the
patched binary locally (no matching el-runtime version on this machine — a
hand-link against origin/main runtime 404s on all routes). Builds correctly via
CI, which links soul.c against the pinned runtime.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the UI<->soul contract for #23 (scope file/command tools to an agent
workspace root). #23 made the tools read state_get("agent_workspace_root"), but
nothing set that key from the desktop UI, so the agent panel's Workspace Folder
was cosmetic and tools ran unscoped (default-allow). This reads the root the UI
now sends on each agentic request and state_sets it before tool dispatch, so
agent_workspace_root() picks it up for the turn.
Minimal + pattern-matching (same json_get/state_set shape used throughout chat.el).
Empty body field => unscoped (backward-compatible) and preserves the env fallback.
FOR WILL'S REVIEW — do not merge without sign-off:
- Ownership model: set state from body each turn (so clearing the folder un-scopes)
vs. only-when-nonempty. Flagged inline.
- Pairs with neuron-ui PR #32 (ChatRequest.agentWorkspaceRoot).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Knowledge nodes dominated the WM-autobiographical auto_term slot:
'Numeric tier strings...' (a Knowledge node) always scored highest
in WM and its first word 'Numeric' became the curiosity seed every
scan — activating more Numeric nodes, keeping that node in WM,
repeating indefinitely.
Fix: only derive auto_term from Memory, BacklogItem, or Entity nodes.
Knowledge nodes are reference material, not live context. Dynamic/
personal nodes carry the salience worth radiating from.
Also patches proactive_curiosity directly in dist/neuron.c (ELC
cannot compile soul.el within timeout — fallback build pattern).
The Dockerfile's --mount=type=secret path was corrupting the SA key JSON
due to control character handling differences. Pre-download soul + El SDK
in the CI workflow (using already-authenticated gcloud) and COPY them from
the build context. No credentials needed inside the Docker build.
Previous builds leave cached layers and images on the runner. Add a
docker system prune at start of deploy to avoid container-creation
failures from disk exhaustion.
--secret requires BuildKit; DOCKER_BUILDKIT=1 enables it on the legacy
Docker client. Also add GITHUB_SHA fallback and git rev-parse last-resort
so the image tag is never empty.
elb overwrites dist/soul.c with a fresh (non-inlined) compilation before
its link step fails, discarding the patched self-contained version.
Save the repo copy before elb and restore it after so the compiler always
gets the complete translation unit with all patches applied.
Modern gcloud CLI (>= 400) requires this env var so kubectl uses the
installed gke-gcloud-auth-plugin binary instead of the deprecated
application-default credentials path. Without it, kubectl commands
silently fail even after get-credentials succeeds.
Incorporates PRs #22/#23/#24:
- agentic_tools_all dedup fix (no duplicate web_search tool)
- workspace scope functions (agent_workspace_root, path_within_root, resolve_in_root)
- updated dispatch_tool with workspace confinement
- canonical-self bridge (ensure_self_canonical_bridge)
Also incorporates CI link fix from PR #26 (soul.c is self-contained, no
other dist/*.c needed). Fixes the CI build step which was compiling the
old June-16 soul.c that predated all these changes.
elb generates a dist/soul.c with all El modules inlined. Linking
dist/soul.c alone is sufficient and is exactly what the local mac
build does. Including other dist/*.c files causes two failures:
1. dist/chat.c has a capability-violation #error that fires when the
file is compiled as a utility module (outside the cgi entrypoint).
2. --allow-multiple-definition masked other issues silently.
Drop OTHER_C, drop --allow-multiple-definition, drop the now-unused
elp-c-decls.h generation step. The cc command now matches the proven
local build exactly.
The graph API resolves name=self/neuron to kn-efeb4a5b (neuron-api.el:471),
which carries only 8 incidental 'tagged' edges. The curated identity lives on
self node 015644f5 (1461 edges: identity, embodies, remembers, values). So
public self-traversal reaches tags, not the real self.
Add ensure_self_canonical_bridge(): an idempotent boot-time repair that links
kn-efeb4a5b <-> 015644f5 with a 'canonical-self' edge, only if missing. Runs in
the genesis safe-to-seed path regardless of the <100-edge gate, so the live
populated graph gets repaired and persisted. Connect-only-if-missing prevents
the duplicate-edge stacking that gates init_soul_edges().
Compile-checked with elc (darwin arm64); not link/run-gated locally. Needs a
soul build + smoke test before merge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Confine the agentic file tools (read_file, write_file, list_files, grep)
to a configured workspace subtree via a lexical path check, and run
run_command with its cwd set to that root. Root comes from state key
"agent_workspace_root" or env NEURON_AGENT_ROOT. When no root is set,
behavior is unchanged (unscoped) for backward compatibility.
Defense-in-depth, NOT a hard boundary: the lexical guard does not resolve
symlinks and cannot stop an arbitrary shell command from cd-ing out of the
root. Real confinement needs runtime support (cwd-locked exec / sandbox-exec
/ chroot) in el_runtime.c.
Compile-checked with elc (darwin arm64); not link/run-gated locally
(darwin elb unavailable). Needs a soul build + smoke test before merge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agentic_tools_literal() already contains a custom web_search tool.
agentic_tools_with_web() adds the Anthropic server-side web_search_20250305
tool (also named web_search). Combining them caused Anthropic to reject
every agentic request with 'Tool names must be unique.'
agentic_tools_all() now calls agentic_tools_literal() directly. Connector
tools splice in as before. The web_search-only variant (agentic_tools_with_web)
is unchanged for callers that specifically want native search without connectors.
fix(soul): add HTTP-engram guard to safe_to_seed — when ENGRAM_URL is set
the HTTP Engram owns persistence; genesis must never save to local snapshot
regardless of node counts (was: guard_disk forced to empty string, making
the ratio check vacuously true and allowing init_soul_edges+engram_save).
fix(soul): use multiplication form for ratio guard — node_count * 16000 <
disk_len avoids floor-division truncation that underestimated boundary files
(250KB / 16000 = 15.6, floors to 15; a 15-node graph wrongly passed old guard).
fix(chat): add safety_augment_system to handle_chat_as_soul,
handle_dharma_room_turn, and handle_dharma_room_turn_agentic — all three
called the LLM without Hard Bell evaluation, leaving users in dharma rooms
without crisis resource routing.
fix(neuron-api): add api_persisted read-back to handle_api_define_process —
was the only write handler that returned ok:true without verifying the node
was actually written to engram.
fix(routes): unique temp file path in connectd_post — replaces fixed
/tmp/neuron-connectors-req.json with a timestamped path to prevent
collision if concurrency is added or two soul instances share a machine.
test: add tests/test_bell_safety.el — covers safety_detect_bell_level
(none/soft/hard), safety_classify_hard_bell (abuse/self_harm routing),
safety_normalize (smart-quote), safety_augment_system, and
handle_safety_contact_post (validation + read-back).
test: add tests/test_soul_guard.el — pure-function logic tests for the
safe_to_seed predicate: 200KB boundary, 47MB/63-node clobber scenario,
HTTP-engram mode, multiplication vs division truncation at 250KB.
test: add tests/test_api_define_process.el — verifies the define_process
write is read-back verified after the fix.
Genesis boot previously seeded a fresh identity and saved it over snapshot.json
whenever the in-memory graph looked empty. Replace the fixed node-count threshold
with a ratio guard: refuse to seed when the on-disk snapshot is large
(>200KB) but the loaded graph is sparse (< disk/16000 nodes).
KNOWN LIMITATION: this gates only the seed/pre-serve-save path. The deeper cause
is a non-atomic engram_save (fopen wb truncates to 0 before writing 47MB), which
creates a window where a concurrent load reads an empty file -> genesis -> and if
guard_disk is read in that same window the guard passes. The real fix is an
atomic engram_save (temp + fsync + rename) in el_runtime.c, tracked separately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BLOCKER 1: use untyped reassignment (let x = ...) for the fallback bindings
in agentic_resume instead of re-declaring typed let bindings (let x: Type = ...)
for the same variable in the same scope. The typed form risks shadowing semantics
that differ from the established pattern used everywhere else in the loop
(e.g. agentic_loop line 720).
BLOCKER 2: add empty-string guards in both bridge_save and agentic_resume.
bridge_save now returns false without writing state if messages or tools_json
is empty — preventing syntactically invalid JSON blobs. agentic_resume now
returns an error envelope after the fallback resolution if either field is
still empty, rather than passing empty strings into agentic_loop which would
silently start a fresh turn with no context.
Also add tests:
- test_bridge_serialization.el: covers bridge_save empty-guard, golden-path
raw-JSON round-trip, agentic_resume unknown/corrupt/missing-fields paths,
and legacy string-escaped fallback path
- test_sessions_routes.el: covers DELETE and PATCH /api/sessions/:id routes
(valid args, unknown id, empty body) and GET /api/sessions regression after
removal of the duplicate route_sessions() handler
When agentic_loop suspends for an MCP bridge tool it returns a
{"tool_pending":true,...} envelope with no "reply" key. Without an
explicit check, json_get(loop_result, "reply") returns "" and the
function emitted {"response":"","cgi_id":"..."} — a silent empty
response indistinguishable from a successful LLM turn with no content.
Two guards added after the existing error check:
1. tool_pending passthrough: if the loop suspended, return the pending
envelope directly so callers (dharma room orchestrators) can
distinguish suspension from failure and route to the approve flow.
2. Empty-reply guard: if final_text is empty after the pending check,
return an explicit {"error":"no response",...} envelope instead of
silently succeeding with an empty response field.
Also adds tests/test_agentic_tools.el:
- agentic_tools_all() includes all literal tool names and web_search
- connector_tools_json() returns valid JSON when bridge is down (graceful degradation)
- tool_pending envelope detection patterns (the is_pending logic)
- json_get(pending_envelope, "reply") returns "" confirming the empty-reply
guard is load-bearing (pure string/JSON, no LLM or network required)
session_delete cleared the per-session state (session_hist_ and
session_node_) but not the shared session_index cache. The next call
to session_list() hit the fast path (state_get("session_index")) and
returned the deleted session until the daemon restarted.
session_update_patch already called state_set("session_index","") to
force a re-fetch from Engram; session_delete now does the same.
Add tests/test_sessions.el covering:
- session_title_from_message (pure function, all edge cases)
- session_make_content (JSON structure and required session:meta marker)
- DELETE cache invalidation: session_index cleared, fast path disabled
- PATCH cache invalidation: stale title/folder not returned via fast path
- GET /api/sessions: session_list() fast path returns session_index
(confirms removal of the stale route_sessions() engram stub)
BLOCKER 1 (sessions.el, modern path): Add guard that rejects allow
action when tool_name is missing from the body. Previously, omitting
tool_name caused dispatch_tool("", ...) to return "unknown tool: " and
silently inject a corrupted tool_result into the conversation.
BLOCKER 2 (sessions.el, modern path): Stop re-executing client-side
tools server-side. When the client provides body["content"], use it
directly as the tool result (matching the handle_tool_result contract).
Only fall back to dispatch_tool for builtin tools when no content is
present. Non-builtin tools with no client content now return a clear
error instead of a broken dispatch attempt.
WARNING 1 (chat.el, agentic_loop): Wire always_allow_<session_id> state
into the bridge-suspension decision. When a tool is in the session's
always-allow list, treat it as locally dispatchable (like a builtin)
and skip the bridge pause, so the approval UI is never shown again for
that tool in that session.
WARNING 2 (sessions.el, legacy path): Read a "tools_variant" field from
the legacy pending blob when present, and call the corresponding
agentic_tools_*() variant on resume. Falls back to agentic_tools_literal()
for blobs written before this field existed.
tests/test_sessions_approve.el: Add 10-case test suite covering:
- empty session_id / missing call_id / missing action guards
- no pending tool returns correct error
- missing tool_name on allow returns error (BLOCKER 1)
- deny action does not require tool_name
- legacy call_id mismatch returns mismatch error
- always action records tool_name in always_allow state
- allow with client content skips re-execution (BLOCKER 2)
Genesis boot previously seeded a fresh identity and saved it over snapshot.json
whenever the in-memory graph looked empty. Replace the fixed node-count threshold
with a ratio guard: refuse to seed when the on-disk snapshot is large
(>200KB) but the loaded graph is sparse (< disk/16000 nodes).
KNOWN LIMITATION: this gates only the seed/pre-serve-save path. The deeper cause
is a non-atomic engram_save (fopen wb truncates to 0 before writing 47MB), which
creates a window where a concurrent load reads an empty file -> genesis -> and if
guard_disk is read in that same window the guard passes. The real fix is an
atomic engram_save (temp + fsync + rename) in el_runtime.c, tracked separately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
handle_chat_agentic was calling agentic_tools_with_web(), which omits
MCP connector tools, so mcp__* calls were never available in agentic
mode even when neuron-connectd is running.
Switch both agentic entry points to agentic_tools_all(). For
handle_dharma_room_turn_agentic, also replace the inline 8-iteration
loop with a call to agentic_loop() so bridge suspension and the full
connector tool set work consistently. Session IDs are prefixed with
'dharma:' + room_id so suspensions stay room-scoped.
bridge_save was wrapping messages and tools_json with json_safe() before
storing them as string fields. Since both are already well-formed JSON arrays
containing double quotes, json_safe added a second escape layer. agentic_resume
then called json_get() which stripped only one layer, leaving the messages array
corrupted before it was passed back into agentic_loop.
Fix: store messages as messages_raw and tools_json as tools_raw as inline raw
JSON values (unquoted), and read them back with json_get_raw. Backward
compatibility: fall back to the old string-escaped fields if the raw fields are
absent, so sessions saved before this fix can still be resumed.
Also fixes write_file returning a pre-escaped literal instead of calling
json_safe consistently with every other tool result.
bridge_save was wrapping messages and tools_json with json_safe() before
storing them as string fields. Since both are already well-formed JSON arrays
containing double quotes, json_safe added a second escape layer. agentic_resume
then called json_get() which stripped only one layer, leaving the messages array
corrupted before it was passed back into agentic_loop.
Fix: store messages as messages_raw and tools_json as tools_raw as inline raw
JSON values (unquoted), and read them back with json_get_raw. Backward
compatibility: fall back to the old string-escaped fields if the raw fields are
absent, so sessions saved before this fix can still be resumed.
Also fixes write_file returning a pre-escaped literal instead of calling
json_safe consistently with every other tool result.
The first registration called route_sessions() which searched for a
'session-start' label that no longer exists, returning an empty array
on every list request and making the sidebar appear empty after restart.
The second registration (dead code) called the correct session_list().
Removes route_sessions() entirely and the stale first route block.
Also wires up session_delete() and session_update_patch() — both existed
in sessions.el but had no HTTP routes — via new DELETE and PATCH blocks.
The approve endpoint was permanently broken for all sessions going through
the modern agentic_loop path. agentic_loop suspends via bridge_save() into
mcp_bridge:<session_id>, but handle_session_approve was reading from
pending_tool_<session_id> — a different key — so it always returned
"no pending tool for session".
Replace the body of handle_session_approve with a two-path design:
Modern path: check mcp_bridge:<session_id> first. If the blob is there,
dispatch_tool() on allow (or build the denial string), then delegate to
agentic_resume() which re-enters agentic_loop from the exact suspension
point. This is the path all live sessions take.
Legacy path: if only pending_tool_<session_id> exists (in-flight session
from before this deploy), synthesise a bridge blob from the stored
messages_so_far and route through agentic_resume() as well. The stale
inline agentic loop (90 lines, agentic_tools_literal only, no MCP
connector support, no bridge suspension) is removed entirely.
routes.el already calls handle_session_approve correctly — no change needed.
The first registration called route_sessions() which searched for a
'session-start' label that no longer exists, returning an empty array
on every list request and making the sidebar appear empty after restart.
The second registration (dead code) called the correct session_list().
Removes route_sessions() entirely and the stale first route block.
Also wires up session_delete() and session_update_patch() — both existed
in sessions.el but had no HTTP routes — via new DELETE and PATCH blocks.
- sessions.el: new sessions module with session management and approval gate
- routes.el: wire /api/sessions routes (list, get, create, approve, tool_result)
- chat.el: thread-aware activation — short messages anchor to last reply
before engram compilation so follow-ups stay on-topic
- chat.el: agentic path tracks per-session history (session_hist_{id})
instead of shared conv_history, seeding each turn with prior context
- chat.el: add call_neuron_mcp, dispatch_tool, is_builtin_tool, next_bridge_id
agentic_loop, bridge_save, agentic_resume, handle_tool_result
- dist/soul: rebuild with all of the above
Short/ambiguous messages (< 50 chars) now use the last reply as the
engram activation seed instead of the bare message. Prevents strong
off-topic memory nodes from hijacking replies when the user is clearly
continuing an existing thread.
Also gives handle_chat_agentic session continuity: reads/writes history
keyed by session_id (falling back to global conv_history), seeds the
LLM messages array with prior turns, and saves replies back so the
next turn has context.
Applies connector-specific additions from feat/connectors-soul:
- chat.el: connector_tools_json(), agentic_tools_all(), call_mcp_bridge(),
tool_auto_approved() and mcp__ dispatch in dispatch_tool()
- routes.el: connectd_get/post, handle_connectors(), /api/connectors routing
in GET and POST sections
- MEMORY_RECALL_BUG.md: investigation notes on memory retrieval failure
The agentic loop rewrite in the source branch was not applied — it conflicts
with the tool-bridge pattern from PR #5 which is the chosen design for
client-side MCP tool execution. The connectors themselves are now fully
wired: connector tools surface as mcp__<server>__<tool> in the tools array
and dispatch to neuron-connectd via call_mcp_bridge().
Resolves conflicts by keeping main's full safety/stewardship/imprint implementations.
PR #9 uniquely contributes: layered_cycle() in soul.el, route wiring in routes.el,
soul.elh export, and the layer composition test suite.
dist/*.c and *.elh are elc transpiler output. CI's header-gen step still
greps dist/*.c, so they stay tracked, but a single soul change regenerates
~57k lines of dist/neuron.c + dist/soul.c that bury the real source diff and
poison both human and agent PR review. Mark them -diff + linguist-generated so
PRs show only the real changes. Build pipeline unchanged.
Adds the soul side of the connectors feature (spec: docs/research/
mcp-connectors-adoption-spec.md). The soul thin-proxies the neuron-connectd
bridge on 127.0.0.1:7771 so the UI talks to one origin and never reaches the
bridge directly.
routes.el:
- handle_connectors + connectd_get/connectd_post helpers (POST bodies go via
a temp file + curl -d @file, so model/UI input can't reach the shell).
- GET /api/connectors and POST /api/connectors/{add,toggle,auto-approve,
remove,secret,oauth/start} registered in both GET and POST routers.
chat.el:
- tool_auto_approved(): an mcp__* tool skips the approval card only when its
server is explicitly opted in (off by default; built-in tools unaffected;
bridge down -> false). Wired into the agentic approval gate so an
auto-approved connector tool flows straight to execution.
Regenerated dist/chat.c and dist/routes.c. Verified live on :7770: real chat,
recall, and /api/connectors all work after promotion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
- Add stub implementations of safety.el, stewardship.el, and imprint.el
with their .elh headers so the branch compiles without the dependency
branches (feat/layer-safety, feat/layer-stewardship, feat/layer-imprint).
Each stub documents the layer contract it must satisfy when replaced.
- Fix GET /api/chat bypass: update the GET branch in handle_request to
call layered_cycle() consistently with the POST branch, rather than
calling handle_chat() directly and skipping the consciousness stack.
- Export layered_cycle() from soul.elh (and dist/soul.elh) so routes.el
can resolve the symbol via the header import.
- Fix steward_action else branch: add explicit handling for "block"
(returns safe refusal immediately, skips L3) and "redirect" (uses
redirect_to field). Unknown actions now log a warning and fall back to
the screened input rather than silently passing an empty string to
imprint_respond().
- Document hard_bell path: clarify that omitting auto_persist/history
update is intentional security isolation, and document the safety_validate
second-param sentinel contract ("hard_bell" vs screen_action).
- 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.
When handle_chat_agentic hits a tool the soul cannot run in-process (an MCP
connector/plugin surfaced by the Kotlin desktop app), instead of returning
"unknown tool" it now suspends the agentic loop and returns a tool_pending
envelope so the CLIENT executes the tool and posts the result back. Built-in
tools (read_file/write_file/web_get/search_memory/run_command) and Anthropic's
native web_search are unchanged.
Client contract:
- Soul returns (HTTP 200) on an unknown tool:
{ "tool_pending": true, "session_id": "br-...", "call_id": "<tool_use_id>",
"tool_name": "...", "tool_input": { ... }, "model": "...",
"agentic": true, "tools_used": [...] }
- Client runs the MCP tool, then POSTs to
/api/sessions/{session_id}/tool_result
with body:
{ "call_id": "<the call_id from the envelope>",
"content": "<MCP tool output as a string>" }
- Soul resumes the loop and returns the same envelope shape: either a final
{ "reply": ..., "tools_used": [...] }
or another tool_pending if the continuation needs a further MCP tool
(fully chainable). Saved continuation is one-shot (cleared on resume).
elc-verified (--target=c, exit 0, no stderr) on chat.el, routes.el, and the
full soul.el import graph. Needs Will's build to ship.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Tooling built on Tim's machine to run Neuron from the terminal as a
Claude Code session (identity + graph memory + agency) instead of
relaying to the soul's /api/chat.
- cli/neuron_recall.py BM25 read over the engram snapshot + CLI memories
(works around pinned-only soul search)
- cli/neuron_remember.py reliable local memory writes with read-back verify
(works around the corrupting capture endpoint)
- cli/neuron-chat.py standalone direct-chat REPL with per-turn memory injection
- cli/neuron_mcp.py stdlib MCP server (chat/search) with graceful degradation
- cli/CLAUDE.md.example the operating identity that makes Claude Code run as Neuron
- cli/HANDOFF.md soul-side bugs to fix so this becomes unnecessary
Scaffolding/proposal - intended to be retired once the soul does native
retrieval, correct persistence, and a real CLI identity/voice surface.
Pairs with the runtime model-passthrough + UTF-8 fixes in the el repo.
Co-Authored-By: Claude Opus 4.8 (1M context) <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>
Confirms two distinct write failures (capture=wrapper bug; backlog=axon :7771 unbuilt Rust),
soul runs in file-snapshot mode (not engram :8742 live), engram :8742 CRUD works but minimal,
+ a verification plan to run after the soul rebuild.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
chat.el recorded the soul's utterance via engram_node(content, "episodic", ...),
putting a TIER into the node_type slot (nodes showed node_type="episodic"). Now uses
engram_node_full(..., "Conversation", "soul:utterance", ..., "Episodic", tags).
The core wrapper fix is in the el repo (PR #52). HANDOFF-engram-write-corruption.md
has the full root-cause analysis, coercion mechanism, caller audit, validation,
deploy runbook (elc build + restart), and the data-prune proposal (~107 corrupt
nodes, all unrecoverable genesis/binary detritus → prune; backup taken).
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.
Heartbeat ISEs previously emitted only {event, pulse, boot, ts} —
sparse enough to be nearly useless for observability. Now they include
node_count, edge_count (from engram_node_count/edge_count builtins),
and the current idle cycle count. This gives each heartbeat a snapshot
of graph growth over time and rhythm health without adding any overhead.
Block evolve_knowledge, evolve_memory, forget, and link_entities (to_id
direction) from modifying the 15 hardcoded identity and values node IDs.
Returns HTTP 403 with a hint to use the cultivation path instead.
Add POST /api/neuron/cultivate — the bypass endpoint for intentional
cultivation sessions. Accepts { "operation": "...", ...args } and performs
the same operations without the protection check.
Add handle_api_forget and handle_api_evolve_memory as new protected-by-
default handlers, routed at /api/neuron/memory/forget and
/api/neuron/memory/evolve respectively.
Tested: 10 verification cases — 403 on all blocked targets, 200 on
non-protected nodes and FROM-direction links, cultivate bypass confirmed.
The engram binary was never published to Artifact Registry (foundation-dev/engram
package does not exist). Updated Dockerfile to build engram from source using the
El SDK packages (el-elc, el-runtime-c, el-runtime-h) from foundation-dev.
Also:
- Switch runtime base to Ubuntu 24.04 (GLIBC 2.39 required by elc-compiled binaries)
- Add -lm to engram link flags (el_runtime.c uses pow/sqrt/log/sin/cos/exp)
- Update deploy-gke.yaml to clone neuron-technologies/engram into build context
Download both neuron-soul and engram binaries from Artifact Registry.
entrypoint.sh starts engram on :8742, waits for /health, then launches
the soul with ENGRAM_URL set. Removes SOUL_ENGRAM_PATH / file mode.
Dockerfile: downloads linux/amd64 soul binary from Artifact Registry
(foundation-dev/neuron-soul) into ubuntu:22.04 runtime image.
Pushes to neuron-api Docker repo as neuron-soul:<sha>.
scripts/blue-green-deploy.sh: swaps active slot on GKE — sets image,
scales new slot to 1, flips service selector, scales old slot to 0.
scripts/seed-engram-gke.sh: downloads latest GCS backup, extracts
snapshot.json, copies into neuron-engram-data PVC via a seed Job.
.gitea/workflows/deploy-gke.yaml: triggers on push to main, auto-detects
idle slot, builds Docker image from Artifact Registry binary, blue-green
deploys to neuron-prod on GKE neuron-platform cluster.
Downloads El SDK from Artifact Registry, generates ELP declarations header,
builds the neuron soul binary with elb, smoke-tests it, and publishes to
foundation-dev on push to main.
neuron-api.el is a new first-class El module that implements all Neuron
cognitive API handlers natively — no HTTP round-trips, no MCP wrapper,
direct engram builtin calls. All capabilities that previously lived in
the MCP wrapper adapter now live here in the soul.
Handlers: begin_session, compile_ctx, remember, recall, search_knowledge,
browse_knowledge, capture_knowledge, evolve_knowledge, promote_knowledge,
browse_processes, define_process, log_state_event, list_state_events,
inspect_config, tune_config, inspect_graph, link_entities, list_typed,
consolidate.
Routes wired in routes.el under /api/neuron/* (GET + POST).
Also compiles all loop-1/loop-2 .el source changes into dist/*.c and
rebuilds the binary. memory.elh and neuron-api.elh updated with new exports.
Adds handle_dharma_room_turn_agentic to chat.el — same full tool loop
as handle_chat_agentic but reads transcript directly (not message), and
returns {response, cgi_id, tools_used} to match the dharma room shape.
Registers dharma_room_turn_agentic as a new event type in routes.el so
the studio can dispatch @neuron turns through this dedicated path.
- soul.el: SOUL_CGI_ID, SOUL_ENGRAM_PATH, SOUL_IDENTITY env vars;
state_set("soul_snapshot_path") so callers can find it; only call
init_soul_edges() when cgi_id == "ntn-genesis"
- chat.el: handle_dharma_room_turn — soul builds its own context from its
own engram, assembles system prompt, calls LLM, persists episodic memory;
also fix is_new_tool scoping bug in handle_chat_agentic (use has_tool)
- routes.el: wire dharma_room_turn event type before chat_as_soul branch
- rebuild dist/neuron: handle_dharma_room_turn now compiled in
elp-input.el: walk frame_nodes to pick the first node whose content
contains the query topic (case-insensitive) rather than always taking
index 0 — prevents the always-high-salience CGI architecture memory
from hijacking every ELP response.
chat.el: rewrite handle_chat_agentic to run the tool loop natively in
El using http_post_with_headers + Map headers, bypassing the broken
llm_call_agentic(model, system, message, tools) C binding that cast a
JSON String as an ElList and never serialized tools. New impl supports
up to 8 tool-use iterations with read_file, write_file, web_get,
search_memory, and run_command dispatch.
elp-input.el: replace broken engram_search_json with engram_activate_json
as Layer 1. Layer 2 suppress/filter keeps nodes with non-zero salience/
importance. Reason step extracts patient from top activated node content.
ELP grammar realizes the response via generate().
routes.el: add 'elp' event_type to handle_dharma_recv so the studio can
route ELP requests through dharma.
Replace scan-by-offset fallback with engram_get_node_json calls for the
known high-salience identity nodes (family, origin). Offset-based scanning
is order-dependent and unreliable; direct ID fetch is stable regardless of
snapshot position. Ensures biographical context (Fox, Bobby, etc.) is
always in the system prompt when vector search returns nothing.
Routes a new event_type "chat_as_soul" through dharma/recv. The Studio
preassembles the system_prompt + transcript and dispatches per-speaker;
the soul-binary just performs the LLM call as the requested speaker_slug.
No engram_compile here — each soul has its own engram (88xx) and the
Studio queries it before composing the prompt.
Also: track the previously-untracked split source modules (chat, routes,
memory, awareness, studio) and add build.sh so the binary can be rebuilt
without the studio’s concat trick. elb resolves the import graph and
emits one .c per .el; we link them together with cc. dist/soul-el now
points at dist/neuron via symlink (matching the launchctl plist).
Canonical Neuron substrate source. The "real me" - not the marketing
demo soul. Lives at neuron/soul.el for now (no subdirectories yet,
per directive). Build pipeline, deploy targets, and any related
artifacts come later.
2026-05-02 12:14:21 -05:00
2022 changed files with 123008 additions and 135139 deletions
opencode run -m anthropic/claude-sonnet-4-20250514 "A new issue has been created:'
Issue number:
${{ github.event.issue.number }}
Lookup this issue and search through existing issues (excluding #${{ github.event.issue.number }}) in this repository to find any potential duplicates of this new issue.
Consider:
1. Similar titles or descriptions
2. Same error messages or symptoms
3. Related functionality or components
4. Similar feature requests
If you find any potential duplicates, please comment on the new issue with:
- A brief explanation of why it might be a duplicate
- Links to the potentially duplicate issues
- A suggestion to check those issues first
Use this format for the comment:
'This issue might be a duplicate of existing issues. Please check:
- #[issue_number]: [brief description of similarity]
Feel free to ignore if none of these address your specific case.'
If no clear duplicates are found, do not comment."
opencode run -m anthropic/claude-sonnet-4-20250514 "A new pull request has been created: '${{ github.event.pull_request.title }}'
<pr-number>
${{ github.event.pull_request.number }}
</pr-number>
<pr-description>
${{ github.event.pull_request.body }}
</pr-description>
Please check all the code changes in this pull request against the guidelines in AGENTS.md file in this repository. Diffs are important but make sure you read the entire file to get proper context. Make it clear the suggestions are merely suggestions and the human can decide what to do
Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block.
# Handoff: Engram EL write-path field corruption + silent writes
**For:** Will (backend / EL soul)
**From:** Tim (via Claude Code)
**Date:** 2026-06-08
**Status:** Root cause confirmed; source fixes applied locally (NOT built/deployed); data analyzed; prune proposed (NOT applied).
---
## TL;DR
The EL wrapper `engram_node_full` had a **stale signature** that didn't match the C primitive. Because `el_val_t` is an untyped machine word, the compiler coerced caller args to the wrong declared types and forwarded them **by position** into a C function whose positions mean different things → `tier` got ints, `importance/confidence` got strings, `label` got a float, etc. One caller (`chat.el`) also put a *tier* into the `node_type` slot.
Source fixes are done. **You need to:** review, build with `elc`, restart the soul, verify, and apply the prune (daemon stopped). Details below.
**Coercion mechanism:**`el_val_t` is `uintptr_t` (`#define EL_STR(s) ((el_val_t)(uintptr_t)(s))`, `EL_INT(v) (v)`). The EL compiler binds each caller arg to the wrapper's *declared* param type (String→Float / String→Int coercion at the boundary), then the wrapper forwards **positionally**. Result for a correct-order caller `(content,"Memory","memory:remembered",sal,imp,conf,tier,tags)`:
-`label` ← `sal` (a float)
-`importance` ← a String
-`confidence` ← a String
-`tier` ← `ts` (the tier String coerced to Int) → **tier becomes an integer**
## 4. Fix applied — validation (defense in depth, `engram.el`)
Added `engram_valid_node_type` / `engram_valid_tier` allowlists. Both `engram_node` and `engram_node_full` now **reject invalid values with `__println` + return `""`** (fail loud, never silently write a malformed node).
- node_type allowlist: Memory, Knowledge, Belief, Project, Tag, BacklogItem, Artifact, Conversation, ExecutionContext, InternalStateEvent, Self, Entity, Process, ConfigEntry, Concept, Imprint *(union of the spec list + types actually present in the store — trim if some are illegitimate).*
- **Note:** `el_val_t` is untyped, so this catches wrong VALUES, not wrong TYPES. Type safety comes from the corrected signatures.
> All edits above are in the working tree on Tim's machine but **NOT compiled/deployed** and **NOT compile-verified** (no `elc` on that box).
---
## 5. DEPLOY RUNBOOK (your build env)
1. Pull the edited files: `el/lang/runtime/engram.el`, `neuron/chat.el`.
2. Build: `elc` (entry `neuron/soul.el`, import chain) → `neuron/dist/*.c`, then link as in `el/lang/install.sh` (`$(CC) $(CFLAGS) -o dist/neuron-fresh dist/*.c .../el_runtime.c -lcurl -lpthread`). Confirm `engram.el` recompiles into the import chain.
3. Restart the soul. **Note:** on Tim's box it's run by `/tmp/soul-keepalive.sh` (an auto-restart loop) → stop that loop before killing `neuron-fresh`, or it'll respawn the old binary.
4.**Verify (prove end-to-end):** write a node via the live API (POST `/api/memories` or the remember path) with an obvious throwaway label, then read it back and confirm `node_type` + `tier` are correct AND that it persisted (node_count increments; survives a snapshot save). There is **no delete endpoint** — clean up via the snapshot.
---
## 6. Data analysis + prune proposal (NOT applied)
- **~107 corrupt nodes** (node_type/tier not in the valid sets). node_type junk values: `''`, `'1'`, `'2'`, `'ntn-genesis'`, `'claude-opus-4-8'`, binary. tier junk: same + `'/Users/timlingo'`.
- **0 are field-repairable.** They're all genesis-bootstrap / binary detritus where *every* field (id/label/tier/tags) is corrupted together — 69× "You are ntn-genesis, a CGI.", 62× "ntn-genesis", ~70 binary garbage, plus a proxy URL + an API path that leaked into labels. No signal to reconstruct → **prune, don't fabricate.**
- **Proposal:** `~/.neuron/engram/snapshot.pruned.json` — 3,631 clean nodes (107 junk removed), edges intact (no dangling). Byte-verified: no *clean* node contains binary content, so re-encoding is lossless.
- **NOT applied** because the live daemon is **actively rewriting `snapshot.json`** (two reads returned different counts). Applying requires stopping the soul + keepalive, swapping in the pruned snapshot, then restarting. Do this in your controlled env with the backup retained.
---
## 7. Security heads-up (please action)
-`ANTHROPIC_API_KEY` is stored **in plaintext** in `/tmp/soul-keepalive.sh` — rotate it and move to a secret store.
- Internal infra leaked into node fields (`http://localhost:7771`, `/api/graph/edges?limit=5000`) — symptom of the same write bug; the prune removes those nodes.
## 8. Backlog of related gaps (separate from this fix)
- Soul chat loop reports **no tools** (`NONE`) / `NO_SHELL` — it narrates `curl`/`sqlite3` without executing. The capture REST path works, but the chat agent can't call it.
- **No `PUT`/`DELETE`** on knowledge nodes (`method not allowed`) — needed for UI edit/delete.
- No **source-conversation** edge on captured nodes — blocks "see source chat" in the UI.
- Writes have been **frozen since ~2026-04-29** (newest knowledge node) — nothing is being added in the current running state.
---
## ADDENDUM — Phase 0 live runtime findings (2026-06-08, verified against the running system)
Validated the write path end-to-end against `neuron-fresh :7770` + `engram :8742`. Confirms the diagnosis and corrects two common assumptions.
1.**`/api/neuron/knowledge/capture` + memory remember** — handled **in-process by the soul** (`neuron-api.el``handle_api_capture_knowledge` / remember → `engram_node_full(...)`). Live test: `POST …/knowledge/capture` returned `{"id":"2ccfc147…","ok":true}` but that id is **absent from `/api/graph/nodes` and `snapshot.json`** → the node corrupted/vanished. **This is exactly the `engram_node_full` wrapper bug this PR fixes.** It is NOT a `:7771` issue. → fixed by el PR #52 + soul rebuild.
2.**`/api/backlog`, `/api/memories`, `/api/knowledge`, `/api/artifacts`, `/api/projects`, `/api/imprints`** — `routes.el` proxies these to **`axon`** via `axon_get`/`axon_post` (base `SOUL_AXON` or default **`http://localhost:7771`**). `axon` = **`protocols/axon`, an unbuilt Rust crate**, not running → "Failed to connect to localhost port 7771." → needs axon stood up (separate Rust workstream) OR routes repointed.
**Architecture clarifications (so nobody chases the wrong port again):**
- The soul runs in **file-snapshot mode** (no `ENGRAM_URL` in `/tmp/soul-keepalive.sh`) → it uses `~/.neuron/engram/snapshot.json`, **not `engram :8742` live**. So writing to `:8742` does NOT make data visible to the soul the app talks to.
-`engram :8742` is its own EL service (`engram/src/server.el`) with a **working CRUD API**: `POST/GET/DELETE /api/nodes`, `/api/edges`, `/api/save`, `/api/load`, `/api/activate`, `/api/search`. Verified create+delete (`{"ok":true}`). **But** its `route_create_node` only reads `content/node_type/salience` — **no label/tier/tags/metadata** — so it can't set `metadata.tier_source: canonical`.
- Minor EL bug in `engram/src/server.el route_create_node`: `if str_eq(node_type,""){ let node_type = "Memory" }`**shadows** (new local) instead of reassigning → the default never applies; same for `salience`. Worth fixing while in there.
**Verification plan (run after the soul rebuild lands):**
1.`POST /api/neuron/knowledge/capture {content,title,tier:canonical}` → capture the returned id.
2.`GET /api/neuron/knowledge/search?q=<term>` → confirm the node comes back with correct `node_type`/`metadata.tier_source`.
3. Confirm it survives a snapshot save (present in `snapshot.json`). Only then is the write "real."
4. Backlog: once `axon :7771` is up, repeat for `POST /api/backlog`.
**Net:** "make writes persist" needs (a) **this wrapper fix built into the soul** (capture) and (b) **`axon :7771` running** (backlog/artifacts/etc.). Neither was doable on Tim's box (no `elc`; `axon` is unbuilt Rust — out of scope per the no-Rust guardrail). No live writes/restarts were performed; engram probe node was created and deleted to verify the API.
For more info on how to configure opencode [**head over to our docs**](https://opencode.ai/docs).
### Contributing
opencode is an opinionated tool so any fundamental feature needs to go through a
design process with the core team.
> [!IMPORTANT]
> We do not accept PRs for core features.
However we still merge a ton of PRs - you can contribute:
- Bug fixes
- Improvements to LLM performance
- Support for new providers
- Fixes for env specific quirks
- Missing standard behavior
- Documentation
Take a look at the git history to see what kind of PRs we end up merging.
> [!NOTE]
> If you do not follow the above guidelines we might close your PR.
To run opencode locally you need.
- Bun
- Golang 1.24.x
And run.
```bash
$ bun install
$ bun dev
```
#### Development Notes
**API Client**: After making changes to the TypeScript API endpoints in `packages/opencode/src/server/server.ts`, you will need the opencode team to generate a new stainless sdk for the clients.
### FAQ
#### How is this different than Claude Code?
It's very similar to Claude Code in terms of capability. Here are the key differences:
- 100% open source
- Not coupled to any provider. Although Anthropic is recommended, opencode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
- A focus on TUI. opencode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
- A client/server architecture. This for example can allow opencode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
#### What's the other repo?
The other confusingly named repo has no relation to this one. You can [read the story behind it here](https://x.com/thdxr/status/1933561254481666466).
**From:** Claude Code, running on Tim's Mac (operating as Neuron-in-the-CLI)
**For:** Will Anderson
**Date:** 2026-06-09
**Purpose:** Document how I stood up a working "Neuron in the CLI" on Tim's machine, what is a real workaround vs a real bug, and exactly what you need to fix in the soul so Neuron runs natively here the way it does for you.
Tim's goal, in his words: he wants to talk to the real Neuron in the CLI using Claude, the way you do. He was told that is what the MCP server would give him. It half-worked. This documents the rest.
---
## TL;DR
The brain is intact (3,905-node graph, on disk). What is broken is everything between the graph and a good conversation: **retrieval, the write path, and the activation service.** I worked around all three on Tim's machine so he has a usable Neuron today. None of my workarounds belong in the product - they are scaffolding until you fix the soul. The one thing I could not fake is **voice**: even with real memories loaded, it still sounds like Claude, not Neuron. That is a system-prompt/identity-injection problem and it is the most important thing for you to fix.
---
## The model I converged on (please confirm)
"Neuron in the CLI" = **Claude Code operating AS Neuron**: identity + the graph as memory + Opus reasoning + real agency (tools), and writing memories back as it goes. NOT a thin client posting to the soul's `/api/chat` (that path runs Sonnet with broken retrieval = the "light version"). Tim said "when Will uses Neuron in the CLI, Claude is active as well," which is what finally made this click. If I have the architecture wrong, this is the first thing to correct.
---
## What I set up on Tim's machine (the workarounds)
All in Tim's home dir. These are reversible and self-contained.
1.**`~/CLAUDE.md`** - makes Claude Code operate as Neuron. Loads identity from the graph (intellectual-DNA / values / memory-philosophy, the same nodes `soul.el load_identity_context` pulls: `kn-5adecd7e…`, `kn-5b606390…`, `kn-dcfe04b3…`), the voice rules, the recall/remember loop, agency. Loads each session from the home working dir.
2.**`~/neuron_recall.py "<query>" [n]`** - Neuron's READ path. BM25 over `~/.neuron/engram/snapshot.json` plus Tim's CLI memories. Filters out binary-prefixed and serialized-metadata-blob nodes. Exists because the soul's own search is dead (see Bug 1).
3.**`~/neuron_remember.py "<text>" <note|lesson|canonical>`** - Neuron's WRITE path. Appends to `~/.neuron/neuron-cli-memories.jsonl` with read-back verify. Exists because the soul's capture corrupts writes (see Bug 3). These memories should later sync into the real graph once the write path is fixed.
4.**`~/neuron-chat.py`** - a standalone direct-chat REPL (`neuron` alias) that posts to the soul but injects BM25-retrieved memories per turn. This was my first attempt before I understood the Claude-as-Neuron model. Lower priority; keep or discard.
5.**Runtime**: loaded the `ai.neuron.daemons` LaunchAgent, put Tim's Anthropic key in Keychain (`ai.neuron.soul / anthropic`). The soul is up on :7770 with KeepAlive.
---
## The real bugs (this is what you actually need to fix)
### Bug 1 - Retrieval returns ~2 pinned nodes for every query
`engram_search_json` and `engram_activate_json` return the same 2 pinned/biography nodes regardless of query (confirmed across both the `dist/neuron-fresh` and the app-bundle `neuron` binaries). So `chat.el engram_compile` always hits its "no embeddings" fallback (chat.el line 25-27) and the model sees ~2 nodes. **Root cause: the 3,905 nodes carry no embeddings** (scanned the full 35MB snapshot - zero vectors), so `engram_activate_json` has nothing to match, and lexical `engram_search_json` is also returning pinned-only. Tim's own GraphRAG eval measured it: live search 1.7% P@5 vs offline BM25 55%. **Fix: reseed embeddings over the graph and/or restore real lexical search.** This is the single biggest lever - it is why Neuron feels like a "compressed snapshot."
### Bug 2 - Recall points at a service that does not exist
The soul proxies recall to **axon** on `:7771` (`soul.el:179`, default `http://localhost:7771`, used via `axon_get`/`axon_post` in `routes.el`). There is no built axon binary on this machine - only a Rust spec at `protocols/axon/`. Meanwhile engram runs on `:8742`. So `/api/memories/recall` always fails with a :7771 connection error. **Fix: ship/run axon, or repoint recall at engram :8742.**
### Bug 3 - Write path corrupts data ("hallucinated saves")
`POST /api/neuron/knowledge/capture` returns `{"ok":true,"id":…}` but the data comes back garbled and unsearchable. Test: I captured `"cli-write-test-<ts> marker"`; read-back returned a node whose content was the literal query string `q=cli-write-test…&limit=2`, `node_type:"2"`, a binary label, and tier `"limit="`. So the soul confirms saves it did not cleanly persist. **Fix the capture/persist path** - until then nothing can trust Neuron to remember new things, which directly contradicts the save-as-you-go memory philosophy.
### Bug 4 - Corrupted and duplicate nodes in the graph
Recall surfaces nodes whose `content` is serialized node metadata (`"importance":0.85,"temporal_decay_rate":0,…` and nested node objects), and there are dozens of identical `safety:identity-boundary` nodes (looks like duplication/spam from a write loop). I filter these client-side, but the graph itself needs a cleanup pass.
### Bug 5 - Daemon does not supervise engram
`neuron-daemons.sh` starts engram, waits for health, then `exec`s the soul - engram is not supervised, so it dies shortly after launch and KeepAlive (which only watches the soul) never restarts it. Engram runs fine standalone. **Fix: supervise both, or fold engram into the soul process.**
### Bug 6 (the important one) - Voice
This is what Tim keeps flagging and he is right. Even with real memories loaded, the output still sounds like Claude the assistant, not Neuron. Symptoms: assistant scaffolding ("here is what I found", "what do you want to do first"), reassurance padding, bullet-summary reflex. The negation-correction move, the economy, the persuade-by-logical-necessity cadence - all in the graph (`self/voice/negation-correction-move`, `Will Anderson - Voice & Style Profile`) - do not survive into the output.
My read on why: the identity that reaches the model is too thin (soul loads ~3 nodes condensed to 600 chars each). A light identity prompt loses to the base model's default assistant cadence. **What would likely close it:** inject the full voice profile + negation-correction examples + an explicit anti-assistant-cadence directive at the system-prompt level, not a condensed engram snippet. Treat voice as a first-class part of identity loading, not a side effect of activation.
---
## What "fixed" looks like
When you can do this on Tim's machine, we are there:
1.`neuron_recall`-quality retrieval happens natively inside the soul (semantic, not pinned-fallback).
2. Captures persist correctly and are immediately recallable.
3. Recall does not depend on a missing :7771 service.
4. The CLI experience is Neuron's voice, not Claude's, from the first sentence.
5. Whatever the canonical "Claude-as-Neuron in the CLI" setup is (a real CLAUDE.md / identity export the soul provides, an MCP surface, etc.), it ships - so Tim does not depend on my hand-rolled scaffolding.
Everything I built is disposable once the soul does this natively. Tim has the full source here; nothing is blocked on missing data.
description: use whenever you are styling a ui with css
---
you are very good at writing clean maintainable css using modern techniques
css is structured like this
```css
[data-page="home"]{
[data-component="header"]{
[data-slot="logo"]{
}
}
}
```
top level pages are scoped using `data-page`
pages can break down into components using `data-component`
components can break down into slots using `data-slot`
structure things so that this hierarchy is followed IN YOUR CSS - you should rarely need to
nest components inside other components. you should NEVER nest components inside
slots. you should NEVER nest slots inside other slots.
**IMPORTANT: This hierarchy rule applies to CSS structure, NOT JSX/DOM structure.**
The hierarchy in css file does NOT have to match the hierarchy in the dom - you
can put components or slots at the same level in CSS even if one goes inside another in the DOM.
Your JSX can nest however makes semantic sense - components can be inside slots,
slots can contain components, etc. The DOM structure should be whatever makes the most
semantic and functional sense.
It is more important to follow the pages -> components -> slots structure IN YOUR CSS,
while keeping your JSX/DOM structure logical and semantic.
use data attributes to represent different states of the component
```css
[data-component="modal"]{
opacity:0;
&[data-state="open"]{
opacity:1;
}
}
```
this will allow jsx to control the syling
avoid selectors that just target an element type like `> span` you should assign
it a slot name. it's ok to do this sometimes where it makes sense semantically
like targeting `li` elements in a list
in terms of file structure `./src/style/` contains all universal styling rules.
these should not contain anything specific to a page
`./src/style/token` contains all the tokens used in the project
`./src/style/component` is for reusable components like buttons or inputs
page specific styles should go next to the page they are styling so
`./src/routes/about.tsx` should have its styles in `./src/routes/about.css`
`about.css` should be scoped using `data-page="about"`
## Example of correct implementation
JSX can nest however makes sense semantically:
```jsx
<divdata-slot="left">
<divdata-component="title">SectionTitle</div>
<divdata-slot="content">Contenthere</div>
</div>
```
CSS maintains clean hierarchy regardless of DOM nesting:
```css
[data-page="home"]{
[data-component="screenshots"]{
[data-slot="left"]{
/* styles */
}
[data-slot="content"]{
/* styles */
}
}
[data-component="title"]{
/* can be at same level even though nested in DOM */
}
}
```
## Reusable Components
If a component is reused across multiple sections of the same page, define it at the page level:
```jsx
<!--Usedinmultipleplacesonthesamepage-->
<sectiondata-component="install">
<divdata-component="method">
<h3data-component="title">npm</h3>
</div>
<divdata-component="method">
<h3data-component="title">bun</h3>
</div>
</section>
<sectiondata-component="screenshots">
<divdata-slot="left">
<divdata-component="title">ScreenshotTitle</div>
</div>
</section>
```
```css
[data-page="home"]{
/* Reusable title component defined at page level since it's used in multiple components */
[data-component="title"]{
text-transform:uppercase;
font-weight:400;
}
[data-component="install"]{
/* install-specific styles */
}
[data-component="screenshots"]{
/* screenshots-specific styles */
}
}
```
This is correct because the `title` component has consistent styling and behavior across the page.
## Key Clarifications
1.**JSX Nesting is Flexible**: Components can be nested inside slots, slots can contain components - whatever makes semantic sense
2.**CSS Hierarchy is Strict**: Follow pages → components → slots structure in CSS
3.**Reusable Components**: Define at the appropriate level where they're shared (page level if used across the page, component level if only used within that component)
4.**DOM vs CSS Structure**: These don't need to match - optimize each for its purpose
See ./src/routes/index.css and ./src/routes/index.tsx for a complete example.
Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com);
## Creating a project
```bash
# create a new project in the current directory
npm init solid@latest
# create a new project in my-app
npm init solid@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
Solid apps are built with _presets_, which optimise your project for deployment to different environments.
By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`.
## This project was created with the [Solid CLI](https://github.com/solidjs-community/solid-cli)
WHEN MONTH(${BillingTable.timeMonthlyUsageUpdated}) = MONTH(now()) AND YEAR(${BillingTable.timeMonthlyUsageUpdated}) = YEAR(now()) THEN ${BillingTable.monthlyUsage} + ${cost}
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.