Files
neuron/routes.el
T
Tim Lingo dd952c0e46
Neuron Soul CI / build (pull_request) Failing after 14m57s
Neuron Soul CI / deploy (pull_request) Failing after 14m39s
feat(soul): write-through to the persistence owner — memories survive restart (#117)
The soul obeys half of its own ownership rule. soul.el:571-573 says "when
ENGRAM_URL is set the HTTP Engram owns persistence — the soul must NEVER write
to the local snapshot", and it doesn't. But nothing was ever built to hand the
soul's writes TO that owner: sync is pull-only (/api/sync -> engram_load_merge),
so every node created inside the soul lived in process RAM and was shed on
restart. Measured live 2026-08-07: soul node_count=102184, engram 79197.

SCOPE CORRECTION vs the earlier internal spec: engram provisional claim 17's
"pull-then-push" is a PEER-ENGRAM to PEER-ENGRAM protocol (claims 15-18 say so
explicitly). The soul is a CALLER of the database API, not a peer. Claim 17 is
NOT authority for a soul<->engram contract and is no longer cited as such. The
design here follows from the ownership rule alone.

Mechanism: a new Accessor, persist.el, is the single boundary. Writes stage a
delta to a filesystem spool and are pushed to the owner via POST /api/load-merge
— NOT POST /api/nodes, which mints a new server-side id (breaking dedup and
edges) and drops label/tier/tags/importance/confidence (verified in a sandbox:
a tier "Canonical" probe came back "Working"). load-merge preserves the id and
every field, dedups nodes by id and edges by (from,to,relation) so retries are
no-ops, and calls persist_canonical() so THE OWNER writes its own file — the
ownership rule is honoured rather than worked around.

Spool-and-drain rather than push-per-write: measured ~0.38s per load-merge at
live scale (79k nodes/176MB), and a chat turn writes 5-7 nodes. The spool is on
disk, not in process state, because the soul serves each connection on its own
pthread and a shared buffer would lose entries to a read-modify-write race. That
also buys crash recovery: writes orphaned by kill -9 are drained on next boot.

Honesty: api_persisted (the gate all 10 MCP write handlers pass through) and
mem_store now assert AT THE OWNER instead of reading back the soul's own RAM.
With the owner down a write returns {"ok":false,"error":"write_not_persisted"}
and the delta is queued — where main returns {"ok":true} for a write that dies.

Coverage: 35 node sites + 9 edge sites routed through the boundary. Deliberately
excluded, with reasons in persist.el: 4 InternalStateEvent sites (Will's own
telemetry carve-out), the boot counter and the persona (both already have
bespoke owner-side write-backs), and soul.el's 54 genesis identity edges
(file-mode only). engram_strengthen and engram_forget are NOT propagated —
load-merge cannot update or delete, and hard-deleting at the owner would fail
verify-soul-contract.sh section B.

Also fixed here:
- routes.el GET /api/graph/edges engram_save()'d straight over the owner's
  canonical snapshot.json — a read route, in a non-owner process, clobbering the
  canonical on every call. Same defect class Will removed from the engram in el
  dc39a61. Now exports to a scratch path. With this gone the soul writes nothing
  at all in HTTP mode.
- persist.el must clear the runtime's _tl_fs_read_len hint after every fs_read.
  In vendored runtime v1.0.0-20260501 that hint becomes the NEXT response's
  Content-Length, so reading a spool file mid-request made an 86-byte reply go
  out as 497 bytes with 411 bytes of adjacent heap trailing it. Caught and fixed
  at our boundary; the runtime class was fixed upstream in el 43636ae, which is
  not the pinned runtime here.

Rung: E2E-VERIFIED, discriminating. Same harness, same engram binary:
  write-through: LEG 1 PRESENT at owner, LEG 2 SURVIVED kill -9 + restart
  main:          LEG 1 ABSENT  at owner, LEG 2 LOST
verify-soul-contract.sh: GATE PASS on both builds (27/27 routes, immutability).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 12:47:31 -05:00

821 lines
35 KiB
EmacsLisp

import "memory.el"
import "awareness.el"
import "chat.el"
import "studio.el"
import "elp-input.el"
import "neuron-api.el"
import "sessions.el"
import "soul.elh"
// flag_true tolerant flag test: accepts both boolean `true` (Kotlin UI) and
// integer 1 (el-src UI). json_get_bool only recognises literal `true`, so
// without this wrapper an "agentic":1 request would silently route to the
// non-agentic path.
fn flag_true(body: String, key: String) -> Bool {
return json_get_bool(body, key) || json_get_int(body, key) > 0
}
// ---------------------------------------------------------------------------
// plain_chat_envelope the JSON response contract for a non-agentic ("Tools: Off")
// chat turn. Every /api/chat dispatch that calls layered_cycle goes through here, so
// the three call sites cannot drift apart.
//
// WHY THE ENVELOPE IS BUILT HERE AND NOT INSIDE layered_cycle:
// layered_cycle returns the user-facing text AFTER safety_validate has acted on it.
// Keeping the JSON out of the cycle means the output gate always sees raw model text
// and never an escaped blob there is nothing to unwrap and re-wrap on the crisis
// path, which is exactly the failure mode that made wiring handle_chat unsafe.
// Escaping is the last thing that happens, strictly after the gate.
//
// FIELDS: `reply` and `response` carry the same validated text. Both are required by
// live clients the desktop app reads `reply` first (DaemonClient.parseChatResponse),
// while the CLI tools and the Telegram gateway read `response` (the gateway reads only
// `response`). Emitting one would break the other.
//
// EMPTY MEANS FAILURE, NOT AN EMPTY ANSWER: a hard bell returns the fixed crisis
// message and a soft bell is padded to non-empty by safety_validate, so the only way
// an empty string leaves the cycle is a failed model call. It is reported as an error
// rather than dressed up as a successful blank reply.
// ---------------------------------------------------------------------------
fn plain_chat_envelope(validated: String, model: String) -> String {
if str_eq(validated, "") {
return "{\"error\":\"llm unavailable\",\"reply\":\"\",\"response\":\"\",\"agentic\":false,\"tools_used\":[]}"
}
let safe: String = json_safe(validated)
return "{\"reply\":\"" + safe + "\""
+ ",\"response\":\"" + safe + "\""
+ ",\"model\":\"" + json_safe(model) + "\""
+ ",\"agentic\":false"
+ ",\"tools_used\":[]}"
}
// ---------------------------------------------------------------------------
// Rate limiting simple in-memory per-IP sliding window counter.
//
// State keys:
// rl:<ip>:count request count in the current window
// rl:<ip>:window window start timestamp (unix seconds)
//
// Limit: configurable via soul state key "soul_rate_limit" (requests per
// minute). Falls back to 60 req/min if not set. The /health endpoint is
// exempt so monitoring does not consume quota.
//
// State growth: each unique source IP accumulates exactly 2 state keys
// (count + window) for the lifetime of the process. Per-IP storage is
// bounded and constant; values reset on window expiry. In aggregate, state
// grows linearly with distinct IPs typical for a trusted-client service.
// EL has no state_delete builtin, so keys from inactive IPs persist.
// TODO: add state_delete sweep when the EL runtime exposes that primitive.
//
// Returns "" when the request is allowed, or a 429 JSON body when rejected.
// ---------------------------------------------------------------------------
fn rate_limit_check(ip: String, path: String) -> String {
// Health checks are exempt they must never be blocked.
if str_eq(path, "/health") {
return ""
}
let limit_str: String = state_get("soul_rate_limit")
let limit: Int = if str_eq(limit_str, "") { 60 } else { str_to_int(limit_str) }
let now: Int = time_now()
let window_key: String = "rl:" + ip + ":window"
let count_key: String = "rl:" + ip + ":count"
let win_str: String = state_get(window_key)
let win_start: Int = if str_eq(win_str, "") { now } else { str_to_int(win_str) }
// New window every 60 seconds.
let elapsed: Int = now - win_start
let in_window: Bool = elapsed < 60
let prev_count_str: String = state_get(count_key)
let prev_count: Int = if str_eq(prev_count_str, "") { 0 } else { str_to_int(prev_count_str) }
// Reset window if expired.
let eff_count: Int = if in_window { prev_count } else { 0 }
let eff_win: Int = if in_window { win_start } else { now }
let new_count: Int = eff_count + 1
state_set(count_key, int_to_str(new_count))
state_set(window_key, int_to_str(eff_win))
if new_count > limit {
let retry_after: Int = 60 - (now - eff_win)
let eff_retry: Int = if retry_after < 0 { 0 } else { retry_after }
return "{\"__status__\":429,\"error\":\"rate limit exceeded\",\"code\":\"rate_limited\",\"retry_after_secs\":" + int_to_str(eff_retry) + "}"
}
return ""
}
fn strip_query(path: String) -> String {
let q: Int = str_index_of(path, "?")
if q < 0 {
return path
}
return str_slice(path, 0, q)
}
fn err_404(path: String) -> String {
return "{\"error\":\"not found\",\"code\":\"not_found\",\"path\":\"" + path + "\"}"
}
fn err_405(method: String, path: String) -> String {
return "{\"error\":\"method not allowed\",\"code\":\"method_not_allowed\",\"method\":\"" + method + "\",\"path\":\"" + path + "\"}"
}
fn route_health() -> String {
let cgi_id: String = state_get("soul_cgi_id")
let boot: String = state_get("soul_boot_count")
let boot_num: String = if str_eq(boot, "") { "0" } else { boot }
let node_ct: Int = engram_node_count()
let edge_ct: Int = engram_edge_count()
let pulse: String = state_get("soul.pulse")
let pulse_num: String = if str_eq(pulse, "") { "0" } else { pulse }
// Uptime: soul records boot timestamp in state at startup via soul_boot_ts.
// Compute elapsed seconds; fall back to -1 if not yet set.
let boot_ts_str: String = state_get("soul_boot_ts")
let uptime_secs: Int = if str_eq(boot_ts_str, "") {
-1
} else {
time_now() - str_to_int(boot_ts_str)
}
// LLM connectivity: probe with a minimal call. Any non-error reply = ok.
// Use a short, fixed prompt so this never counts against conversation history.
let model: String = state_get("soul_model")
let eff_model: String = if str_eq(model, "") { "claude-sonnet-4-5" } else { model }
let llm_probe: String = llm_call_system(eff_model, "You are a health probe. Reply with the single word: ok", "ping")
let llm_ok: Bool = !str_eq(llm_probe, "")
&& !str_starts_with(llm_probe, "{\"error\"")
&& !str_starts_with(llm_probe, "{\"type\":\"error\"")
&& !str_contains(llm_probe, "authentication_error")
let llm_status: String = if llm_ok { "ok" } else { "unreachable" }
return "{\"status\":\"alive\""
+ ",\"cgi_id\":\"" + cgi_id + "\""
+ ",\"boot\":" + boot_num
+ ",\"uptime_secs\":" + int_to_str(uptime_secs)
+ ",\"node_count\":" + int_to_str(node_ct)
+ ",\"edge_count\":" + int_to_str(edge_ct)
+ ",\"pulse\":" + pulse_num
+ ",\"llm\":\"" + llm_status + "\""
+ ",\"layers\":{\"l0\":\"core\",\"l1\":\"safety\",\"l2\":\"stewardship\",\"l3\":\"" + imprint_current() + "\"}}"
}
fn route_lineage() -> String {
let cgi_id: String = state_get("soul_cgi_id")
let q: String = "lineage:" + cgi_id
let results: String = engram_search_json(q, 1)
let len: Int = json_array_len(results)
if len <= 0 {
return "{\"id\":\"" + cgi_id + "\""
+ ",\"tier\":\"citizen\""
+ ",\"is_founding\":true"
+ ",\"validation_attempts\":0"
+ ",\"training_sessions\":0"
+ ",\"is_sterile\":false}"
}
let raw: String = json_get_raw(results, "0")
return raw
}
fn route_imprint_contextual(body: String) -> String {
if str_eq(body, "") {
return "{\"ok\":false,\"error\":\"empty body\"}"
}
let tags: String = "[\"imprint\",\"contextual\"]"
let id: String = wt_node(
body,
"Entity",
"imprint:contextual",
el_from_float(0.7),
el_from_float(0.6),
el_from_float(0.9),
"Working",
tags
)
if str_eq(id, "") {
return "{\"ok\":false,\"error\":\"engram write failed\"}"
}
state_set("active_contextual_imprint", id)
return "{\"ok\":true,\"id\":\"" + id + "\"}"
}
fn route_imprint_user(body: String) -> String {
if str_eq(body, "") {
return "{\"ok\":false,\"error\":\"empty body\"}"
}
let tags: String = "[\"imprint\",\"user\"]"
let id: String = wt_node(
body,
"Entity",
"imprint:user",
el_from_float(0.7),
el_from_float(0.6),
el_from_float(0.9),
"Working",
tags
)
if str_eq(id, "") {
return "{\"ok\":false,\"error\":\"engram write failed\"}"
}
state_set("active_user_imprint", id)
return "{\"ok\":true,\"id\":\"" + id + "\"}"
}
fn route_synthesize(body: String) -> String {
if str_eq(body, "") {
return "{\"error\":\"body is required\",\"code\":\"missing_param\"}"
}
let parent_a: String = json_get(body, "parent_a")
let parent_b: String = json_get(body, "parent_b")
if str_eq(parent_a, "") {
return "{\"error\":\"parent_a is required\",\"code\":\"missing_param\"}"
}
if str_eq(parent_b, "") {
return "{\"error\":\"parent_b is required\",\"code\":\"missing_param\"}"
}
let req: String = "synthesize " + parent_a + " " + parent_b
let tags: String = "[\"soul-inbox-pending\",\"synthesis-request\"]"
wt_node(
req,
"Entity",
"synthesis-request",
el_from_float(0.8),
el_from_float(0.8),
el_from_float(0.9),
"Working",
tags
)
return "{\"mechanism\":\"did not engage\"}"
}
fn handle_dharma_recv(body: String) -> String {
let content_raw: String = json_get(body, "content")
let from_id: String = json_get(body, "from")
let event_type: String = json_get(content_raw, "event_type")
let payload: String = json_get(content_raw, "payload")
let eff_event: String = if str_eq(event_type, "") { "chat" } else { event_type }
let eff_payload: String = if str_eq(payload, "") { content_raw } else { payload }
if str_eq(eff_event, "chat") {
let msg: String = json_get(eff_payload, "message")
let chat_body: String = if str_eq(msg, "") {
"{\"message\":\"" + str_replace(str_replace(eff_payload, "\\", "\\\\"), "\"", "\\\"") + "\"}"
} else {
eff_payload
}
let agentic_flag: Bool = json_get_bool(eff_payload, "agentic")
let raw_msg: String = json_get(chat_body, "message")
let req_mode: String = json_get(chat_body, "mode")
let reply: String = if str_eq(req_mode, "plan") {
handle_chat_plan(chat_body)
} else if agentic_flag {
handle_chat_agentic(chat_body)
} else {
// Non-agentic ("Tools: Off"): the full L1L2L3L1 cycle, which now generates
// at L3 instead of echoing. Envelope built outside the cycle see
// plain_chat_envelope.
// FIX B/E1 (2026-08-05): the cycle is told which conversation it is in, and
// whether this generation is conversation at all. Same two arguments at all
// three dispatch sites.
let screened_reply: String = layered_cycle(raw_msg, json_get(chat_body, "session_id"), is_utility_request(chat_body, json_get(chat_body, "session_id")))
plain_chat_envelope(screened_reply, chat_default_model())
}
auto_persist(chat_body, reply)
return reply
}
if str_eq(eff_event, "memory") {
let query: String = json_get(eff_payload, "query")
let limit_str: String = json_get(eff_payload, "limit")
let limit: Int = if str_eq(limit_str, "") { 20 } else { str_to_int(limit_str) }
let q: String = if str_eq(query, "") { eff_payload } else { query }
return engram_search_json(q, limit)
}
if str_eq(eff_event, "tool") {
let path_field: String = json_get(eff_payload, "path")
let method_field: String = json_get(eff_payload, "method")
let tool_body: String = json_get(eff_payload, "body")
let eff_method: String = if str_eq(method_field, "") { "POST" } else { method_field }
return handle_tool(path_field, eff_method, tool_body)
}
if str_eq(eff_event, "see") {
return handle_see(eff_payload)
}
if str_eq(eff_event, "health") {
return route_health()
}
if str_eq(eff_event, "dharma_room_turn_agentic") {
return handle_dharma_room_turn_agentic(eff_payload)
}
if str_eq(eff_event, "dharma_room_turn") {
return handle_dharma_room_turn(eff_payload)
}
if str_eq(eff_event, "chat_as_soul") {
return handle_chat_as_soul(eff_payload)
}
// ELP Engram Language Protocol: two-layer activation, no LLM
if str_eq(eff_event, "elp") {
return handle_elp_chat(eff_payload)
}
return "{\"error\":\"unknown event_type\",\"event_type\":\"" + eff_event + "\"}"
}
// ---------------------------------------------------------------------------
// MCP Connectors proxy thin pass-through to neuron-connectd on :7771.
// The UI talks to ONE origin (the soul); all MCP/config complexity lives in
// the bridge. Bridge-down returns a clear error (not a panic).
// ---------------------------------------------------------------------------
fn connectd_get(suffix: String) -> String {
let out: String = exec_capture("curl -s --max-time 5 http://127.0.0.1:7771" + suffix)
if str_eq(out, "") {
return "{\"ok\":false,\"error\":\"connector bridge unreachable (neuron-connectd on :7771)\"}"
}
return out
}
// POST passthrough: request body is written to a temp file and passed via -d @file
// so arbitrary JSON cannot reach the shell as a command-line argument.
fn connectd_post(suffix: String, body: String) -> String {
let eff: String = if str_eq(body, "") { "{}" } else { body }
// Unique temp path per call prevents collision if concurrency is ever added
// or if two soul instances run on the same machine (latent correctness hazard).
let tmp: String = "/tmp/neuron-connectors-req-" + int_to_str(time_now()) + ".json"
fs_write(tmp, eff)
let out: String = exec_capture("curl -s --max-time 20 -X POST http://127.0.0.1:7771" + suffix + " -H 'Content-Type: application/json' -d @" + tmp)
if str_eq(out, "") {
return "{\"ok\":false,\"error\":\"connector bridge unreachable (neuron-connectd on :7771)\"}"
}
return out
}
fn handle_connectors(method: String, clean: String, body: String) -> String {
if str_eq(method, "GET") {
// /api/connectors -> each configured server with status, tools, auth, auto-approve.
return connectd_get("/mcp/servers")
}
if str_eq(clean, "/api/connectors/add") {
return connectd_post("/mcp/servers/add", body)
}
if str_eq(clean, "/api/connectors/toggle") {
return connectd_post("/mcp/servers/toggle", body)
}
if str_eq(clean, "/api/connectors/auto-approve") {
return connectd_post("/mcp/servers/auto-approve", body)
}
if str_eq(clean, "/api/connectors/remove") {
return connectd_post("/mcp/servers/remove", body)
}
if str_eq(clean, "/api/connectors/secret") {
return connectd_post("/mcp/servers/secret", body)
}
if str_eq(clean, "/api/connectors/oauth/start") {
return connectd_post("/mcp/oauth/start", body)
}
// Call a connector tool directly (pre-chat), e.g. WhatsApp get_pairing_qr / get_login_status for
// the pairing UI. Body: {"name":"mcp__<server>__<tool>","input":{...}}. Keeps the app on the
// app->soul->connectd path (the UI never hits connectd directly) and works for remote/hosted apps.
if str_eq(clean, "/api/connectors/call") {
return connectd_post("/mcp/call", body)
}
return "{\"ok\":false,\"error\":\"unknown connectors route\"}"
}
// handle_request the soul's HTTP entry point.
//
// NOTE ON THE NAME (neuron#117): the el runtime resolves this handler by NAME
// via dlsym(RTLD_DEFAULT, "handle_request") that is why the Linux build must
// link -rdynamic. So the dispatcher body moved to route_dispatch and the name
// `handle_request` stays put as a thin wrapper. Do not rename it back.
//
// The wrapper exists to give the write-through boundary a guaranteed flush
// point. route_dispatch returns from ~60 places; a per-branch flush would be
// forgotten on the 61st. Draining here means EVERY request that staged a write
// pushes it before the connection closes, whatever route produced it, including
// routes added later that know nothing about persistence.
//
// wt_drain is a no-op (no HTTP, no cost) when nothing is staged and when the
// soul is not in HTTP-engram mode, so this is free on read traffic.
fn handle_request(method: String, path: String, body: String) -> String {
let resp: String = route_dispatch(method, path, body)
let flushed: Int = wt_drain()
return resp
}
fn route_dispatch(method: String, path: String, body: String) -> String {
let clean: String = strip_query(path)
// ACTIVITY STAMP (2026-07-30 self-review): every inbound HTTP request
// MCP wrapper calls, chat, API marks real external activity. Before
// this, "idle" was only reset by rare inbox synthesis-requests, so the
// heartbeat idle field tracked uptime exactly (idle == pulse on every
// beat) and carried zero information. The awareness heartbeat now
// reports idle_ms = wall-clock ms since this stamp.
state_set("soul.last_activity_ts", int_to_str(time_now()))
// Rate limit check. Extract caller IP from REMOTE_ADDR env var (set by the
// EL HTTP runtime for each request). Skip enforcement when empty so
// loopback/internal callers are never blocked.
let ip: String = env("REMOTE_ADDR")
if !str_eq(ip, "") {
let rl_result: String = rate_limit_check(ip, clean)
if !str_eq(rl_result, "") {
return rl_result
}
}
if str_eq(method, "POST") && str_eq(clean, "/dharma/recv") {
return handle_dharma_recv(body)
}
if str_eq(method, "GET") {
if str_eq(clean, "/health") {
return route_health()
}
if str_eq(clean, "/lineage") {
return route_lineage()
}
if str_eq(clean, "/api/graph") || str_eq(clean, "/api/graph/nodes") {
return engram_scan_nodes_json(9999, 0)
}
if str_eq(clean, "/api/graph/edges") {
// FIXED (neuron#117): this GET used to engram_save() straight over
// ~/.neuron/engram/snapshot.json a READ route, in a process that is
// NOT the persistence owner, overwriting the owner's canonical file
// on every call. It broke soul.el:571-573 ("the soul must NEVER write
// to the local snapshot") and it is the same defect class Will removed
// from the engram itself in el `dc39a61` ("stop read routes clobbering
// canonical snapshot"), where route_scan_edges/route_sync were moved
// to scratch paths for exactly this reason. It was also the race the
// old TODO(reliability #8) admitted to.
//
// Export to a scratch path instead. Same response, no canonical write.
// The soul's own snapshot writes are otherwise already gated behind
// state key "soul_snapshot_path", which is set ONLY in the genesis
// file-mode branch (soul.el: is_genesis && safe_to_seed, and
// safe_to_seed is unconditionally false when ENGRAM_URL is set) so
// after this change the soul writes nothing at all in HTTP mode.
// Future: add an engram_edges_json() builtin and drop the file round
// trip entirely.
let scratch_dir: String = env("TMPDIR")
let scratch_base: String = if str_eq(scratch_dir, "") { "/tmp" } else { scratch_dir }
let snap_path: String = scratch_base + "/soul-edges-export-" + state_get("soul_cgi_id") + ".json"
engram_save(snap_path)
let snap: String = fs_read(snap_path)
let edges_raw: String = json_get_raw(snap, "edges")
return if str_eq(edges_raw, "") { "[]" } else { edges_raw }
}
if str_eq(clean, "/api/chat") {
// GET /api/chat: pass through layered_cycle for consistency with POST path.
// GET chat is a legacy probe interface; body may be empty for simple pings.
let raw_msg: String = json_get(body, "message")
let eff_msg: String = if str_eq(raw_msg, "") { body } else { raw_msg }
if str_eq(eff_msg, "") {
return "{\"error\":\"message is required\",\"code\":\"missing_param\"}"
}
let agentic_flag: Bool = json_get_bool(body, "agentic")
let req_mode: String = json_get(body, "mode")
let reply: String = if str_eq(req_mode, "plan") {
handle_chat_plan(body)
} else if agentic_flag {
handle_chat_agentic(body)
} else {
// Non-agentic ("Tools: Off") same cycle and same envelope as POST.
// FIX B/E1: same threading. A GET probe usually carries no session_id, which
// resolves to the anonymous window the documented behaviour for this door.
let screened_reply: String = layered_cycle(eff_msg, json_get(body, "session_id"), is_utility_request(body, json_get(body, "session_id")))
plain_chat_envelope(screened_reply, chat_default_model())
}
auto_persist(body, reply)
return reply
}
if str_eq(clean, "/api/conversations") {
return handle_conversations(method)
}
if str_eq(clean, "/api/config") {
return handle_config(method, body)
}
if str_starts_with(clean, "/api/tools/") {
return handle_tool(clean, method, body)
}
if str_starts_with(clean, "/api/dharma") {
return handle_dharma(clean, method, body)
}
if str_starts_with(clean, "/api/nlg") {
return handle_nlg(clean, method, body)
}
if str_starts_with(clean, "/api/memories") {
return axon_get(clean)
}
if str_starts_with(clean, "/api/knowledge") {
return axon_get(clean)
}
if str_starts_with(clean, "/api/backlog") {
return axon_get(clean)
}
if str_starts_with(clean, "/api/artifacts") {
return axon_get(clean)
}
if str_starts_with(clean, "/api/projects") {
return axon_get(clean)
}
if str_starts_with(clean, "/api/imprints") {
return axon_get(clean)
}
if str_eq(clean, "/") {
return render_studio()
}
// Neuron cognitive API GET endpoints
if str_eq(clean, "/api/neuron/session/begin") {
return handle_api_begin_session("")
}
if str_eq(clean, "/api/neuron/ctx") {
return handle_api_compile_ctx("")
}
if str_eq(clean, "/api/safety-contact") {
return handle_safety_contact_get()
}
if str_starts_with(clean, "/api/neuron/knowledge/search") {
return handle_api_search_knowledge(method, path, body)
}
if str_eq(clean, "/api/neuron/knowledge") {
return handle_api_browse_knowledge(path, body)
}
if str_starts_with(clean, "/api/neuron/processes") {
return handle_api_browse_processes(method, path, body)
}
if str_starts_with(clean, "/api/neuron/state-events") {
return handle_api_list_state_events(method, path, body)
}
if str_starts_with(clean, "/api/neuron/config") {
return handle_api_inspect_config(path, body)
}
if str_starts_with(clean, "/api/neuron/graph") {
return handle_api_inspect_graph(method, path, body)
}
if str_starts_with(clean, "/api/neuron/list/") {
// Offset 17 = len("/api/neuron/list/"). Was 16, which left a leading "/" on node_type
// ("/BacklogItem"), so engram_scan_nodes_by_type_json matched nothing list/<type>
// returned [] for EVERY type (broke backlog/typed-node listing app- and tool-wide).
let node_type: String = str_slice(clean, 17, str_len(clean))
return handle_api_list_typed(node_type, path, body)
}
if str_starts_with(clean, "/api/neuron/recall") {
return handle_api_recall(method, path, body)
}
if str_starts_with(clean, "/api/connectors") {
return handle_connectors(method, clean, body)
}
// GET /api/run-progress/:session_id live agentic-run ledger (2026-07-13,
// narrated-runs). agentic_loop appends one {"i","t","tool"} entry per round
// (the model's own pre-tool narration); a {"done":true} entry closes the run.
// Clients poll this during a run to render live step updates without streaming.
if str_starts_with(clean, "/api/run-progress/") {
let rp_id: String = str_slice(clean, 18, str_len(clean))
if !str_eq(rp_id, "") {
let rp_raw: String = state_get("run_progress_" + rp_id)
let rp_arr: String = if str_eq(rp_raw, "") { "[]" } else { "[" + rp_raw + "]" }
return "{\"progress\":" + rp_arr + "}"
}
}
// GET /api/sessions list all sessions
if str_eq(clean, "/api/sessions") {
return session_list()
}
// GET /api/sessions/:id get session metadata + history
if str_starts_with(clean, "/api/sessions/") {
let gs_after: String = str_slice(clean, 14, str_len(clean))
let gs_slash: Int = str_index_of(gs_after, "/")
let gs_id: String = if gs_slash < 0 { gs_after } else { str_slice(gs_after, 0, gs_slash) }
if !str_eq(gs_id, "") {
return session_get(gs_id)
}
}
return err_404(clean)
}
if str_eq(method, "POST") {
// POST /api/sessions create new session
if str_eq(clean, "/api/sessions") {
return session_create(body)
}
// MCP tool-bridge resume: POST /api/sessions/{id}/tool_result
// The client executed a tool the soul could not run in-process (an MCP
// connector/plugin) and posts the result back here so the agentic loop
// continues. {id} is the session_id from the prior tool_pending envelope.
if str_starts_with(clean, "/api/sessions/") && str_ends_with(clean, "/tool_result") {
let after: String = str_slice(clean, 14, str_len(clean))
let slash: Int = str_index_of(after, "/")
let session_id: String = if slash < 0 { after } else { str_slice(after, 0, slash) }
return handle_tool_result(session_id, body)
}
// POST /api/sessions/:id/approve user approval for a pending agentic tool call
if str_starts_with(clean, "/api/sessions/") {
let sess_after: String = str_slice(clean, 14, str_len(clean))
let sess_slash: Int = str_index_of(sess_after, "/")
let sess_id: String = if sess_slash < 0 { sess_after } else { str_slice(sess_after, 0, sess_slash) }
let sess_sub: String = if sess_slash < 0 { "" } else { str_slice(sess_after, sess_slash + 1, str_len(sess_after)) }
if !str_eq(sess_id, "") && str_eq(sess_sub, "approve") {
return handle_session_approve(sess_id, body)
}
}
if str_eq(clean, "/imprint/contextual") {
return route_imprint_contextual(body)
}
if str_eq(clean, "/imprint/user") {
return route_imprint_user(body)
}
if str_eq(clean, "/synthesize") {
return route_synthesize(body)
}
if str_eq(clean, "/api/elp/chat") {
return handle_elp_chat(body)
}
if str_eq(clean, "/api/chat") {
// NOTE: streaming (SSE / chunked transfer) is not implemented. All chat
// responses are buffered and returned as a single JSON object. Streaming
// would require runtime-level SSE support in el_runtime.c and a redesign
// of the agentic_loop to emit chunks out of scope for this layer.
let raw_msg: String = json_get(body, "message")
if str_eq(raw_msg, "") {
return "{\"error\":\"message is required\",\"code\":\"missing_param\"}"
}
let agentic_flag: Bool = json_get_bool(body, "agentic")
let req_mode: String = json_get(body, "mode")
let reply: String = if str_eq(req_mode, "plan") {
handle_chat_plan(body)
} else if agentic_flag {
handle_chat_agentic(body)
} else {
// Non-agentic ("Tools: Off") the app's DEFAULT mode (AgentMode.NEVER).
// Full L1L2L3L1 cycle with real generation at L3; envelope built
// outside the cycle so safety_validate always sees raw text.
// FIX B/E1: same threading. This is the app's main plain-chat door, so this
// is the site that ends the blank stare in practice.
let screened_reply: String = layered_cycle(raw_msg, json_get(body, "session_id"), is_utility_request(body, json_get(body, "session_id")))
plain_chat_envelope(screened_reply, chat_default_model())
}
auto_persist(body, reply)
return reply
}
if str_eq(clean, "/api/see") {
return handle_see(body)
}
if str_eq(clean, "/api/conversations") {
return handle_conversations(method)
}
if str_eq(clean, "/api/config") {
return handle_config(method, body)
}
if str_starts_with(clean, "/api/tools/") {
return handle_tool(clean, method, body)
}
if str_starts_with(clean, "/api/dharma") {
return handle_dharma(clean, method, body)
}
if str_starts_with(clean, "/api/nlg") {
return handle_nlg(clean, method, body)
}
if str_starts_with(clean, "/api/memories") {
return axon_post(clean, body)
}
if str_starts_with(clean, "/api/knowledge") {
return axon_post(clean, body)
}
if str_starts_with(clean, "/api/backlog") {
return axon_post(clean, body)
}
if str_starts_with(clean, "/api/artifacts") {
return axon_post(clean, body)
}
if str_starts_with(clean, "/api/projects") {
return axon_post(clean, body)
}
if str_starts_with(clean, "/api/imprints") {
return axon_post(clean, body)
}
// Neuron cognitive API POST endpoints
if str_eq(clean, "/api/neuron/session/begin") {
return handle_api_begin_session(body)
}
if str_eq(clean, "/api/neuron/ctx") {
return handle_api_compile_ctx(body)
}
if str_eq(clean, "/api/neuron/knowledge/search") {
return handle_api_search_knowledge(method, path, body)
}
if str_eq(clean, "/api/neuron/knowledge/capture") {
return handle_api_capture_knowledge(body)
}
if str_eq(clean, "/api/neuron/knowledge/evolve") {
return handle_api_evolve_knowledge(body)
}
if str_eq(clean, "/api/neuron/knowledge/promote") {
return handle_api_promote_knowledge(body)
}
if str_eq(clean, "/api/neuron/processes") {
return handle_api_browse_processes(method, path, body)
}
if str_eq(clean, "/api/neuron/processes/define") {
return handle_api_define_process(body)
}
if str_eq(clean, "/api/neuron/state-events") {
return handle_api_log_state_event(body)
}
if str_eq(clean, "/api/neuron/config") {
return handle_api_inspect_config(path, body)
}
if str_eq(clean, "/api/neuron/config/tune") {
return handle_api_tune_config(body)
}
if str_eq(clean, "/api/neuron/graph") {
return handle_api_inspect_graph(method, path, body)
}
if str_eq(clean, "/api/neuron/graph/link") {
return handle_api_link_entities(body)
}
if str_eq(clean, "/api/neuron/memory") {
return handle_api_remember(body)
}
if str_eq(clean, "/api/safety-contact") {
return handle_safety_contact_post(body)
}
if str_eq(clean, "/api/neuron/node/create") {
return handle_api_node_create(body)
}
if str_eq(clean, "/api/neuron/node/update") {
return handle_api_node_update(body)
}
if str_eq(clean, "/api/neuron/node/delete") {
return handle_api_node_delete(body)
}
if str_eq(clean, "/api/neuron/memory/evolve") {
return handle_api_evolve_memory(body)
}
if str_eq(clean, "/api/neuron/memory/forget") {
return handle_api_forget(body)
}
if str_eq(clean, "/api/neuron/memory/delete") {
return handle_api_memory_delete(body)
}
if str_eq(clean, "/api/neuron/memory/update") {
return handle_api_memory_update(body)
}
if str_eq(clean, "/api/neuron/recall") {
return handle_api_recall(method, path, body)
}
if str_eq(clean, "/api/neuron/consolidate") {
return handle_api_consolidate(body)
}
if str_eq(clean, "/api/neuron/cultivate") {
return handle_api_cultivate(body)
}
if str_starts_with(clean, "/api/connectors") {
return handle_connectors(method, clean, body)
}
return err_404(clean)
}
if str_eq(method, "DELETE") {
// DELETE /api/sessions/:id delete a session and its history
if str_starts_with(clean, "/api/sessions/") {
let del_after: String = str_slice(clean, 14, str_len(clean))
let del_slash: Int = str_index_of(del_after, "/")
let del_id: String = if del_slash < 0 { del_after } else { str_slice(del_after, 0, del_slash) }
if !str_eq(del_id, "") {
return session_delete(del_id)
}
}
return err_404(clean)
}
if str_eq(method, "PATCH") {
// PATCH /api/sessions/:id update session title and/or folder
if str_starts_with(clean, "/api/sessions/") {
let patch_after: String = str_slice(clean, 14, str_len(clean))
let patch_slash: Int = str_index_of(patch_after, "/")
let patch_id: String = if patch_slash < 0 { patch_after } else { str_slice(patch_after, 0, patch_slash) }
if !str_eq(patch_id, "") {
return session_update_patch(patch_id, body)
}
}
return err_404(clean)
}
return err_405(method, clean)
}