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)
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.
- 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.
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.