Non-agentic /api/chat (the desktop app's default "Tools: Off" mode) returned the
user's own screened text as a bare non-JSON string. Every JSON client failed to
parse it and showed "Couldn't reach Neuron - it may be offline."
Root cause: f52d5bd (2026-06-11) correctly moved the route onto the layer spine
(handle_chat -> layered_cycle), but L3 never got a generator — imprint_respond()
annotates its input and returns it. Two pieces of the architecture were already
waiting for that step: layered_cycle parks a bell directive in the state key
build_system_prompt is written to consume, and build_system_prompt carries a
chat_mode ("no tools") flag with no live caller.
The fix composes rather than replaces. Wiring handle_chat would have removed
safety_screen, the hard-bell short-circuit, the whole stewardship layer and
safety_validate — the only enforcing output gate in the codebase — in exchange
for a working reply (see _engine-websearch-20260804/SAFETY-STOP.md). Instead
layered_cycle keeps every gate, in order, and gains a generation step between
imprint_respond and safety_validate.
L1 screen -> guard -> hard-bell short-circuit -> L2a -> L2b -> L2c
-> L3 imprint_respond (prompt) -> L3b layered_generate (NEW) -> L1 validate
- chat.el: NEW layered_generate (L3 generation, no tools offered),
conv_history_block, conv_history_record.
FIX build_system_prompt never concatenated no_tools_rule into its
return — the "[NO TOOLS THIS TURN]" rule reached no model at all.
handle_chat annotated DO-NOT-WIRE with the reason.
- soul.el: layered_cycle gains L3b + post-validation turn bookkeeping.
- routes.el: NEW plain_chat_envelope; all three /api/chat dispatch sites wrap the
cycle's output. Built OUTSIDE the cycle so safety_validate always sees
raw model text — nothing to unwrap or rebuild on the crisis path.
Emits both `reply` and `response`: the desktop app reads `reply`,
the CLI tools and telegram-gateway read `response`.
Also fixes BUG-PLAINCHAT-1, a pre-existing CRITICAL crash on the crisis path.
elc compiles `let n: Int = pos + str_len(marker)` to el_str_concat() — string
concat on two integers — inside a block-expression initializer, segfaulting the
daemon (SIGSEGV in strlen). Six inline copies of the same " | ts:" parser had it:
two in layered_cycle L2c, two in engram_compile (live on the AGENTIC path too),
two in affective_context_prefix. A distress turn following an earlier affective
turn killed the whole process. Proven pre-existing: an unmodified baseline binary
crashes identically, and the same bad C is in the committed dist/soul.c. Fixed by
hoisting to one top-level function, affective_node_ts(), where the expression
compiles to integer addition — verified in the generated C.
Proof (throwaway HOME/engram, explicit NEURON_PORT, live chain untouched):
- Plain turn returns a JSON envelope with the provider's answer, not an echo.
- Captured request body: no `tools`, no `tool_choice`; system prompt carries the
NO-TOOLS rule. Tools:Off means no tool is offered, structurally.
- Hard bell: canned 988 message, and the provider request count does not move —
the message never reaches a model.
- Soft bell + a 2-char model reply: safety_validate's care phrase is appended to
the MODEL's output. Output gate acting, on this route.
- The L1 bell directive now reaches the model here for the first time (the state
addendum had a producer and no consumer).
- test_layered_cycle PASS; all six El suites byte-identical to baseline.
- verify-soul-contract.sh (bash 5.3): GATE PASS, 27/27, immutability PASS.
- The crash sequence that killed the baseline daemon now returns HTTP 200.
Not proven: no live Anthropic call — the login keychain refuses the key to a
non-interactive process (rc=24 errSecInteractionNotAllowed). Details and the
one-command close-out are in _engine-plainchat-20260805/README.md §7.
Builds on PR #108. dist/soul.c deliberately not regenerated — Will's toolchain.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 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
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.