cf154387ce
This route called engram_save() over ~/.neuron/engram/snapshot.json — the engram server's CANONICAL store — then fs_read it back, to answer a READ query. A read route overwriting the persistence owner's file. This defect was fixed once before (export redirected to a scratch path). It came back tonight in the @route dispatch conversion: the hand-written dispatch block held the FIXED version, the @route-decorated copy held the unfixed one, and the merge kept the decorated copy. Calling the endpoint afterward overwrote the canonical snapshot and immediately preceded an engram crash. Now calls engram_edges_json(limit, offset) — the builtin the route's own TODO asked for — which reads g->edges directly. No file is written or read. Bounded: limit defaults to 1000, offset supported, so the whole-graph read that fell over is not reachable by default. Verified: same request that previously rewrote snapshot.json now leaves it byte-identical (sha256 unchanged before/after), and returns real edge records with every persisted field.
973 lines
41 KiB
EmacsLisp
973 lines
41 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.
|
|
@utility
|
|
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 ""
|
|
}
|
|
|
|
@utility
|
|
fn strip_query(path: String) -> String {
|
|
let q: Int = str_index_of(path, "?")
|
|
if q < 0 {
|
|
return path
|
|
}
|
|
return str_slice(path, 0, q)
|
|
}
|
|
|
|
@utility
|
|
fn err_404(path: String) -> String {
|
|
return "{\"error\":\"not found\",\"code\":\"not_found\",\"path\":\"" + path + "\"}"
|
|
}
|
|
|
|
@utility
|
|
fn err_405(method: String, path: String) -> String {
|
|
return "{\"error\":\"method not allowed\",\"code\":\"method_not_allowed\",\"method\":\"" + method + "\",\"path\":\"" + path + "\"}"
|
|
}
|
|
|
|
@manager
|
|
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() + "\"}}"
|
|
}
|
|
|
|
@manager
|
|
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
|
|
}
|
|
|
|
@manager
|
|
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 + "\"}"
|
|
}
|
|
|
|
@manager
|
|
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 + "\"}"
|
|
}
|
|
|
|
@manager
|
|
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\"}"
|
|
}
|
|
|
|
@manager
|
|
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 L1→L2→L3→L1 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).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
@accessor
|
|
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.
|
|
@accessor
|
|
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
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// @route DISPATCH — every HTTP route is a @route-decorated handler. The El
|
|
// compiler scans these decorators and synthesizes `el_route_dispatch(method,
|
|
// clean, path, body)`, emitted SPECIFICITY-SORTED (exact > compound > suffix >
|
|
// prefix; longer wins within a class) so overlapping paths never shadow,
|
|
// independent of source order. Matching is on `clean` (query-stripped); the
|
|
// ORIGINAL `path` is passed to handlers so query strings survive. Unmatched →
|
|
// sentinel "__EL_NO_ROUTE__". handle_request (bottom) calls it once, then maps
|
|
// the sentinel to 404 (recognised method) / 405 (unknown method).
|
|
//
|
|
// Adapters carry the uniform (method, path, body) signature. Those that need
|
|
// the query-stripped path recompute `clean = strip_query(path)` internally,
|
|
// exactly as the former hand-written dispatcher did.
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
// ── pre-guard: inter-soul Dharma receive (POST /dharma/recv) ────────────────
|
|
@route("/dharma/recv", "POST", "exact") @manager
|
|
fn r_dharma_recv(method: String, path: String, body: String) -> String {
|
|
return handle_dharma_recv(body)
|
|
}
|
|
|
|
// ── GET: liveness / lineage ─────────────────────────────────────────────────
|
|
@route("/health", "GET", "exact") @manager
|
|
fn r_health(method: String, path: String, body: String) -> String {
|
|
return route_health()
|
|
}
|
|
|
|
@route("/lineage", "GET", "exact") @manager
|
|
fn r_lineage(method: String, path: String, body: String) -> String {
|
|
return route_lineage()
|
|
}
|
|
|
|
// ── GET: raw engram graph (two exact aliases share one helper) ──────────────
|
|
@route("/api/graph", "GET", "exact") @manager
|
|
fn r_api_graph(method: String, path: String, body: String) -> String {
|
|
return engram_scan_nodes_json(9999, 0)
|
|
}
|
|
|
|
@route("/api/graph/nodes", "GET", "exact") @manager
|
|
fn r_api_graph_nodes(method: String, path: String, body: String) -> String {
|
|
return engram_scan_nodes_json(9999, 0)
|
|
}
|
|
|
|
@route("/api/graph/edges", "GET", "exact") @manager
|
|
fn r_api_graph_edges(method: String, path: String, body: String) -> String {
|
|
// Reads edges straight from the store. No file is written or read.
|
|
//
|
|
// This route used to engram_save() the ENTIRE graph over
|
|
// ~/.neuron/engram/snapshot.json — the engram server's CANONICAL store —
|
|
// and then fs_read it back, just to answer a read query. Two defects in
|
|
// one line: a READ route clobbering the persistence owner's canonical
|
|
// file (the defect fixed once already, then reintroduced when the
|
|
// hand-written dispatch block was replaced by @route dispatch and the
|
|
// unfixed copy is the one that survived), and a 128 MB serialize +
|
|
// reread + parse per request. Calling it on 2026-08-15 overwrote the
|
|
// canonical snapshot and preceded an engram crash loop.
|
|
//
|
|
// engram_edges_json is the builtin the old TODO here asked for. Bounded
|
|
// by default (1000) — the unbounded whole-graph read is what fell over.
|
|
let lim: Int = api_query_int(path, "limit", 1000)
|
|
let off: Int = api_query_int(path, "offset", 0)
|
|
return engram_edges_json(lim, off)
|
|
}
|
|
|
|
// ── GET /api/chat — legacy probe interface; body may be empty ───────────────
|
|
@route("/api/chat", "GET", "exact") @manager
|
|
fn r_chat_get(method: String, path: String, body: String) -> String {
|
|
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 {
|
|
let screened_reply: String = layered_cycle(eff_msg, json_get(body, "session_id"), is_utility_request(body, json_get(body, "session_id")))
|
|
screened_reply
|
|
}
|
|
auto_persist(body, reply)
|
|
return reply
|
|
}
|
|
|
|
// ── GET|POST: method-branching handlers (same fn, guards on method) ─────────
|
|
@route("/api/conversations", "GET|POST", "exact") @manager
|
|
fn r_conversations(method: String, path: String, body: String) -> String {
|
|
return handle_conversations(method)
|
|
}
|
|
|
|
@route("/api/config", "GET|POST", "exact") @manager
|
|
fn r_config(method: String, path: String, body: String) -> String {
|
|
return handle_config(method, body)
|
|
}
|
|
|
|
@route("/api/tools/", "GET|POST", "prefix") @manager
|
|
fn r_tools(method: String, path: String, body: String) -> String {
|
|
let clean: String = strip_query(path)
|
|
return handle_tool(clean, method, body)
|
|
}
|
|
|
|
@route("/api/dharma", "GET|POST", "prefix") @manager
|
|
fn r_dharma(method: String, path: String, body: String) -> String {
|
|
let clean: String = strip_query(path)
|
|
return handle_dharma(clean, method, body)
|
|
}
|
|
|
|
@route("/api/nlg", "GET|POST", "prefix") @manager
|
|
fn r_nlg(method: String, path: String, body: String) -> String {
|
|
let clean: String = strip_query(path)
|
|
return handle_nlg(clean, method, body)
|
|
}
|
|
|
|
// ── GET|POST axon proxies (GET → axon_get, POST → axon_post) ────────────────
|
|
@route("/api/memories", "GET|POST", "prefix") @manager
|
|
fn r_memories(method: String, path: String, body: String) -> String {
|
|
let clean: String = strip_query(path)
|
|
return if str_eq(method, "GET") { axon_get(clean) } else { axon_post(clean, body) }
|
|
}
|
|
|
|
@route("/api/knowledge", "GET|POST", "prefix") @manager
|
|
fn r_knowledge_axon(method: String, path: String, body: String) -> String {
|
|
let clean: String = strip_query(path)
|
|
return if str_eq(method, "GET") { axon_get(clean) } else { axon_post(clean, body) }
|
|
}
|
|
|
|
@route("/api/backlog", "GET|POST", "prefix") @manager
|
|
fn r_backlog(method: String, path: String, body: String) -> String {
|
|
let clean: String = strip_query(path)
|
|
return if str_eq(method, "GET") { axon_get(clean) } else { axon_post(clean, body) }
|
|
}
|
|
|
|
@route("/api/artifacts", "GET|POST", "prefix") @manager
|
|
fn r_artifacts(method: String, path: String, body: String) -> String {
|
|
let clean: String = strip_query(path)
|
|
return if str_eq(method, "GET") { axon_get(clean) } else { axon_post(clean, body) }
|
|
}
|
|
|
|
@route("/api/projects", "GET|POST", "prefix") @manager
|
|
fn r_projects(method: String, path: String, body: String) -> String {
|
|
let clean: String = strip_query(path)
|
|
return if str_eq(method, "GET") { axon_get(clean) } else { axon_post(clean, body) }
|
|
}
|
|
|
|
@route("/api/imprints", "GET|POST", "prefix") @manager
|
|
fn r_imprints(method: String, path: String, body: String) -> String {
|
|
let clean: String = strip_query(path)
|
|
return if str_eq(method, "GET") { axon_get(clean) } else { axon_post(clean, body) }
|
|
}
|
|
|
|
// ── GET / — studio UI ───────────────────────────────────────────────────────
|
|
@route("/", "GET", "exact") @manager
|
|
fn r_root(method: String, path: String, body: String) -> String {
|
|
return render_studio()
|
|
}
|
|
|
|
// ── Neuron cognitive API — session/ctx (GET empty arg, POST body) ───────────
|
|
@route("/api/neuron/session/begin", "GET|POST", "exact") @manager
|
|
fn r_session_begin(method: String, path: String, body: String) -> String {
|
|
return if str_eq(method, "GET") { handle_api_begin_session("") } else { handle_api_begin_session(body) }
|
|
}
|
|
|
|
@route("/api/neuron/ctx", "GET|POST", "exact") @manager
|
|
fn r_ctx(method: String, path: String, body: String) -> String {
|
|
return if str_eq(method, "GET") { handle_api_compile_ctx("") } else { handle_api_compile_ctx(body) }
|
|
}
|
|
|
|
@route("/api/safety-contact", "GET|POST", "exact") @manager
|
|
fn r_safety_contact(method: String, path: String, body: String) -> String {
|
|
return if str_eq(method, "GET") { handle_safety_contact_get() } else { handle_safety_contact_post(body) }
|
|
}
|
|
|
|
// ── Neuron cognitive API — knowledge ────────────────────────────────────────
|
|
// GET search is a PREFIX (legacy) while POST search is EXACT — kept distinct so
|
|
// semantics match the former dispatcher byte-for-byte.
|
|
@route("/api/neuron/knowledge/search", "GET", "prefix") @manager
|
|
fn r_knowledge_search_get(method: String, path: String, body: String) -> String {
|
|
return handle_api_search_knowledge(method, path, body)
|
|
}
|
|
|
|
@route("/api/neuron/knowledge/search", "POST", "exact") @manager
|
|
fn r_knowledge_search_post(method: String, path: String, body: String) -> String {
|
|
return handle_api_search_knowledge(method, path, body)
|
|
}
|
|
|
|
@route("/api/neuron/knowledge", "GET", "exact") @manager
|
|
fn r_knowledge_browse(method: String, path: String, body: String) -> String {
|
|
return handle_api_browse_knowledge(path, body)
|
|
}
|
|
|
|
@route("/api/neuron/knowledge/capture", "POST", "exact") @manager
|
|
fn r_knowledge_capture(method: String, path: String, body: String) -> String {
|
|
return handle_api_capture_knowledge(body)
|
|
}
|
|
|
|
@route("/api/neuron/knowledge/evolve", "POST", "exact") @manager
|
|
fn r_knowledge_evolve(method: String, path: String, body: String) -> String {
|
|
return handle_api_evolve_knowledge(body)
|
|
}
|
|
|
|
@route("/api/neuron/knowledge/promote", "POST", "exact") @manager
|
|
fn r_knowledge_promote(method: String, path: String, body: String) -> String {
|
|
return handle_api_promote_knowledge(body)
|
|
}
|
|
|
|
// ── Neuron cognitive API — processes (GET prefix, POST exact + define) ──────
|
|
@route("/api/neuron/processes", "GET", "prefix") @manager
|
|
fn r_processes_get(method: String, path: String, body: String) -> String {
|
|
return handle_api_browse_processes(method, path, body)
|
|
}
|
|
|
|
@route("/api/neuron/processes", "POST", "exact") @manager
|
|
fn r_processes_post(method: String, path: String, body: String) -> String {
|
|
return handle_api_browse_processes(method, path, body)
|
|
}
|
|
|
|
@route("/api/neuron/processes/define", "POST", "exact") @manager
|
|
fn r_processes_define(method: String, path: String, body: String) -> String {
|
|
return handle_api_define_process(body)
|
|
}
|
|
|
|
// ── Neuron cognitive API — state events (GET prefix list, POST exact log) ───
|
|
@route("/api/neuron/state-events", "GET", "prefix") @manager
|
|
fn r_state_events_get(method: String, path: String, body: String) -> String {
|
|
return handle_api_list_state_events(method, path, body)
|
|
}
|
|
|
|
@route("/api/neuron/state-events", "POST", "exact") @manager
|
|
fn r_state_events_post(method: String, path: String, body: String) -> String {
|
|
return handle_api_log_state_event(body)
|
|
}
|
|
|
|
// ── Neuron cognitive API — config (GET prefix, POST exact + tune) ──────────
|
|
@route("/api/neuron/config", "GET", "prefix") @manager
|
|
fn r_config_get(method: String, path: String, body: String) -> String {
|
|
return handle_api_inspect_config(path, body)
|
|
}
|
|
|
|
@route("/api/neuron/config", "POST", "exact") @manager
|
|
fn r_config_post(method: String, path: String, body: String) -> String {
|
|
return handle_api_inspect_config(path, body)
|
|
}
|
|
|
|
@route("/api/neuron/config/tune", "POST", "exact") @manager
|
|
fn r_config_tune(method: String, path: String, body: String) -> String {
|
|
return handle_api_tune_config(body)
|
|
}
|
|
|
|
// ── Neuron cognitive API — graph (GET prefix, POST exact + link) ───────────
|
|
@route("/api/neuron/graph", "GET", "prefix") @manager
|
|
fn r_graph_get(method: String, path: String, body: String) -> String {
|
|
return handle_api_inspect_graph(method, path, body)
|
|
}
|
|
|
|
@route("/api/neuron/graph", "POST", "exact") @manager
|
|
fn r_graph_post(method: String, path: String, body: String) -> String {
|
|
return handle_api_inspect_graph(method, path, body)
|
|
}
|
|
|
|
@route("/api/neuron/graph/link", "POST", "exact") @manager
|
|
fn r_graph_link(method: String, path: String, body: String) -> String {
|
|
return handle_api_link_entities(body)
|
|
}
|
|
|
|
// ── Neuron cognitive API — typed-node list (dynamic :node_type) ─────────────
|
|
// Offset 17 = len("/api/neuron/list/"). str_slice on `clean` so query strings
|
|
// never leak into node_type.
|
|
@route("/api/neuron/list/", "GET", "prefix") @manager
|
|
fn r_list_typed(method: String, path: String, body: String) -> String {
|
|
let clean: String = strip_query(path)
|
|
let node_type: String = str_slice(clean, 17, str_len(clean))
|
|
return handle_api_list_typed(node_type, path, body)
|
|
}
|
|
|
|
// ── Neuron cognitive API — recall (GET prefix, POST exact) ─────────────────
|
|
@route("/api/neuron/recall", "GET", "prefix") @manager
|
|
fn r_recall_get(method: String, path: String, body: String) -> String {
|
|
return handle_api_recall(method, path, body)
|
|
}
|
|
|
|
@route("/api/neuron/recall", "POST", "exact") @manager
|
|
fn r_recall_post(method: String, path: String, body: String) -> String {
|
|
return handle_api_recall(method, path, body)
|
|
}
|
|
|
|
// ── Neuron cognitive API — memory / node writes (POST exact) ────────────────
|
|
@route("/api/neuron/memory", "POST", "exact") @manager
|
|
fn r_memory(method: String, path: String, body: String) -> String {
|
|
return handle_api_remember(body)
|
|
}
|
|
|
|
@route("/api/neuron/memory/evolve", "POST", "exact") @manager
|
|
fn r_memory_evolve(method: String, path: String, body: String) -> String {
|
|
return handle_api_evolve_memory(body)
|
|
}
|
|
|
|
@route("/api/neuron/memory/forget", "POST", "exact") @manager
|
|
fn r_memory_forget(method: String, path: String, body: String) -> String {
|
|
return handle_api_forget(body)
|
|
}
|
|
|
|
@route("/api/neuron/memory/delete", "POST", "exact") @manager
|
|
fn r_memory_delete(method: String, path: String, body: String) -> String {
|
|
return handle_api_memory_delete(body)
|
|
}
|
|
|
|
@route("/api/neuron/memory/update", "POST", "exact") @manager
|
|
fn r_memory_update(method: String, path: String, body: String) -> String {
|
|
return handle_api_memory_update(body)
|
|
}
|
|
|
|
@route("/api/neuron/node/create", "POST", "exact") @manager
|
|
fn r_node_create(method: String, path: String, body: String) -> String {
|
|
return handle_api_node_create(body)
|
|
}
|
|
|
|
@route("/api/neuron/node/update", "POST", "exact") @manager
|
|
fn r_node_update(method: String, path: String, body: String) -> String {
|
|
return handle_api_node_update(body)
|
|
}
|
|
|
|
@route("/api/neuron/node/delete", "POST", "exact") @manager
|
|
fn r_node_delete(method: String, path: String, body: String) -> String {
|
|
return handle_api_node_delete(body)
|
|
}
|
|
|
|
@route("/api/neuron/consolidate", "POST", "exact") @manager
|
|
fn r_consolidate(method: String, path: String, body: String) -> String {
|
|
return handle_api_consolidate(body)
|
|
}
|
|
|
|
@route("/api/neuron/cultivate", "POST", "exact") @manager
|
|
fn r_cultivate(method: String, path: String, body: String) -> String {
|
|
return handle_api_cultivate(body)
|
|
}
|
|
|
|
// ── POST: chat / ELP / see / imprint / synthesize ──────────────────────────
|
|
@route("/api/elp/chat", "POST", "exact") @manager
|
|
fn r_elp_chat(method: String, path: String, body: String) -> String {
|
|
return handle_elp_chat(body)
|
|
}
|
|
|
|
@route("/api/see", "POST", "exact") @manager
|
|
fn r_see(method: String, path: String, body: String) -> String {
|
|
return handle_see(body)
|
|
}
|
|
|
|
@route("/imprint/contextual", "POST", "exact") @manager
|
|
fn r_imprint_contextual(method: String, path: String, body: String) -> String {
|
|
return route_imprint_contextual(body)
|
|
}
|
|
|
|
@route("/imprint/user", "POST", "exact") @manager
|
|
fn r_imprint_user(method: String, path: String, body: String) -> String {
|
|
return route_imprint_user(body)
|
|
}
|
|
|
|
@route("/synthesize", "POST", "exact") @manager
|
|
fn r_synthesize(method: String, path: String, body: String) -> String {
|
|
return route_synthesize(body)
|
|
}
|
|
|
|
// POST /api/chat — buffered (no streaming); message is REQUIRED.
|
|
@route("/api/chat", "POST", "exact") @manager
|
|
fn r_chat_post(method: String, path: String, body: String) -> String {
|
|
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 {
|
|
let screened_reply: String = layered_cycle(raw_msg, json_get(body, "session_id"), is_utility_request(body, json_get(body, "session_id")))
|
|
screened_reply
|
|
}
|
|
auto_persist(body, reply)
|
|
return reply
|
|
}
|
|
|
|
// ── Sessions — list / create / dynamic :id (GET/POST/DELETE/PATCH) ──────────
|
|
@route("/api/sessions", "GET", "exact") @manager
|
|
fn r_sessions_list(method: String, path: String, body: String) -> String {
|
|
return session_list()
|
|
}
|
|
|
|
@route("/api/sessions", "POST", "exact") @manager
|
|
fn r_sessions_create(method: String, path: String, body: String) -> String {
|
|
return session_create(body)
|
|
}
|
|
|
|
// COMPOUND: POST /api/sessions/:id/tool_result — MCP tool-bridge resume. Must
|
|
// out-specify the bare approve prefix (it does: compound > prefix), preserving
|
|
// the load-bearing tool_result-before-approve order of the old dispatcher.
|
|
@route("/api/sessions/", "POST", "compound", "/tool_result") @manager
|
|
fn r_sessions_tool_result(method: String, path: String, body: String) -> String {
|
|
let clean: String = strip_query(path)
|
|
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 — bare prefix + in-handler sub check, exactly
|
|
// as the former dispatcher. Non-"approve" subpaths fall through to 404 (the old
|
|
// code returned nothing and dropped to the POST-block err_404).
|
|
@route("/api/sessions/", "POST", "prefix") @manager
|
|
fn r_sessions_approve(method: String, path: String, body: String) -> String {
|
|
let clean: String = strip_query(path)
|
|
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)
|
|
}
|
|
return err_404(clean)
|
|
}
|
|
|
|
@route("/api/sessions/", "GET", "prefix") @manager
|
|
fn r_sessions_get(method: String, path: String, body: String) -> String {
|
|
let clean: String = strip_query(path)
|
|
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)
|
|
}
|
|
|
|
@route("/api/sessions/", "DELETE", "prefix") @manager
|
|
fn r_sessions_delete(method: String, path: String, body: String) -> String {
|
|
let clean: String = strip_query(path)
|
|
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)
|
|
}
|
|
|
|
@route("/api/sessions/", "PATCH", "prefix") @manager
|
|
fn r_sessions_patch(method: String, path: String, body: String) -> String {
|
|
let clean: String = strip_query(path)
|
|
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)
|
|
}
|
|
|
|
// ── GET /api/run-progress/:session_id — live agentic-run ledger ─────────────
|
|
// Offset 18 = len("/api/run-progress/").
|
|
@route("/api/run-progress/", "GET", "prefix") @manager
|
|
fn r_run_progress(method: String, path: String, body: String) -> String {
|
|
let clean: String = strip_query(path)
|
|
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 + "}"
|
|
}
|
|
return err_404(clean)
|
|
}
|
|
|
|
// ── MCP Connectors — proxy to neuron-connectd :7771 ─────────────────────────
|
|
// GET (any /api/connectors*) → server list. POST sub-routes are exact; an
|
|
// unmatched POST /api/connectors* prefix returns the "unknown connectors route"
|
|
// body, exactly as the former handle_connectors fallthrough.
|
|
@route("/api/connectors", "GET", "prefix") @manager
|
|
fn r_connectors_get(method: String, path: String, body: String) -> String {
|
|
return connectd_get("/mcp/servers")
|
|
}
|
|
|
|
@route("/api/connectors/add", "POST", "exact") @manager
|
|
fn r_connectors_add(method: String, path: String, body: String) -> String {
|
|
return connectd_post("/mcp/servers/add", body)
|
|
}
|
|
|
|
@route("/api/connectors/toggle", "POST", "exact") @manager
|
|
fn r_connectors_toggle(method: String, path: String, body: String) -> String {
|
|
return connectd_post("/mcp/servers/toggle", body)
|
|
}
|
|
|
|
@route("/api/connectors/auto-approve", "POST", "exact") @manager
|
|
fn r_connectors_auto_approve(method: String, path: String, body: String) -> String {
|
|
return connectd_post("/mcp/servers/auto-approve", body)
|
|
}
|
|
|
|
@route("/api/connectors/remove", "POST", "exact") @manager
|
|
fn r_connectors_remove(method: String, path: String, body: String) -> String {
|
|
return connectd_post("/mcp/servers/remove", body)
|
|
}
|
|
|
|
@route("/api/connectors/secret", "POST", "exact") @manager
|
|
fn r_connectors_secret(method: String, path: String, body: String) -> String {
|
|
return connectd_post("/mcp/servers/secret", body)
|
|
}
|
|
|
|
@route("/api/connectors/oauth/start", "POST", "exact") @manager
|
|
fn r_connectors_oauth_start(method: String, path: String, body: String) -> String {
|
|
return connectd_post("/mcp/oauth/start", body)
|
|
}
|
|
|
|
// Call a connector tool directly (pre-chat), e.g. WhatsApp get_pairing_qr /
|
|
// get_login_status. Keeps the app on the app->soul->connectd path.
|
|
@route("/api/connectors/call", "POST", "exact") @manager
|
|
fn r_connectors_call(method: String, path: String, body: String) -> String {
|
|
return connectd_post("/mcp/call", body)
|
|
}
|
|
|
|
@route("/api/connectors", "POST", "prefix") @manager
|
|
fn r_connectors_unknown(method: String, path: String, body: String) -> String {
|
|
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.
|
|
// Dispatches through the compiler-synthesized @route table (el_route_dispatch,
|
|
// below) before falling through to any remaining hand-written routes.
|
|
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
|
|
}
|
|
}
|
|
|
|
// Compiler-synthesized dispatch (specificity-sorted, method-guarded).
|
|
let route_resp: String = el_route_dispatch(method, clean, path, body)
|
|
if !str_eq(route_resp, "__EL_NO_ROUTE__") {
|
|
return route_resp
|
|
}
|
|
|
|
// handle_api_structural_audit lives in neuron-api.el (not routes.el), so it
|
|
// was missed by the @route conversion — kept as an explicit dispatch rather
|
|
// than guessed at cross-file @route semantics. GET uses a prefix match
|
|
// (query string carries sample caps: ?edge_sample=, ?node_sample=, ?edges=0);
|
|
// POST is exact-match, same handler, no body fields read.
|
|
if str_eq(method, "GET") && str_starts_with(clean, "/api/neuron/audit/structural") {
|
|
return handle_api_structural_audit(method, path, body)
|
|
}
|
|
if str_eq(method, "POST") && str_eq(clean, "/api/neuron/audit/structural") {
|
|
return handle_api_structural_audit(method, path, body)
|
|
}
|
|
|
|
// Fallthrough: a recognised method with no matching path → 404; an
|
|
// unrecognised method → 405 (matches the old per-method-block structure).
|
|
if str_eq(method, "GET") || str_eq(method, "POST") || str_eq(method, "DELETE") || str_eq(method, "PATCH") {
|
|
return err_404(clean)
|
|
}
|
|
return err_405(method, clean)
|
|
}
|