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.