Compare commits

...

1 Commits

Author SHA1 Message Date
will.anderson a5f411e739 routes: convert manual dispatch to @route + fix latent Bool/String crashes
Neuron Soul CI / build (pull_request) Failing after 45s
Neuron Soul CI / deploy (pull_request) Failing after 10m28s
Convert routes.el from ~89 hand-written dispatch branches to 75 @route-decorated
handlers with VBD roles (@manager/@accessor/@utility), driven by the modular
compiler's native @route dispatch. routes.elh updated to match.

Fix four latent bugs that the modular compiler surfaces (unlike the old inlining
compiler, it faithfully emits every source statement, so per-module compilation
no longer silently drops code):
  - safety.el: malformed soft-phrases literal (extra unescaped quote)
  - sessions.el: str_replace quote arg + topic_tags JSON escaping
  - memory.el mem_save and neuron-api.el consolidate: engram_save returns a Bool,
    not a String; str_eq(result, "") dereferenced the Bool value (0x1) as a char*,
    segfaulting the awareness loop on boot and POST /api/neuron/consolidate.
    Check it as a Bool (if !save_result). The old compiler dropped these checks,
    masking the bug in the shipped dist/soul.c.

Regenerate dist/soul.c through the bounded per-module path (modular elc per module
plus amalgamation that embeds dist/elp-c-decls.h), never by folding soul.el through
elc (27GB OOM). Verified on a throwaway snapshot and port: boots with the awareness
loop active and no segfault, @route dispatch matches the manual dispatcher across
all sampled routes and the four shadowing-hazard pairs, and consolidate returns 200.
2026-08-10 17:53:17 -05:00
7 changed files with 28026 additions and 22623 deletions
Generated Vendored
+27487 -22245
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -109,8 +109,8 @@ fn mem_consolidate() -> String {
}
fn mem_save(path: String) -> Void {
let save_result: String = engram_save(path)
if str_eq(save_result, "") {
let save_result: Bool = engram_save(path)
if !save_result {
println("[memory] mem_save: engram_save failed for " + path + " — snapshot may be incomplete")
}
}
+2 -2
View File
@@ -656,8 +656,8 @@ fn handle_api_consolidate(body: String) -> String {
let summary: String = json_get(body, "summary")
let snap: String = state_get("soul_snapshot_path")
if !str_eq(snap, "") {
let save_result: String = engram_save(snap)
if str_eq(save_result, "") {
let save_result: Bool = engram_save(snap)
if !save_result {
println("[api] consolidate: engram_save failed for " + snap + " — snapshot may be out of sync")
}
}
+531 -370
View File
@@ -11,6 +11,7 @@ import "soul.elh"
// 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
}
@@ -74,6 +75,7 @@ fn rate_limit_check(ip: String, path: String) -> String {
return ""
}
@utility
fn strip_query(path: String) -> String {
let q: Int = str_index_of(path, "?")
if q < 0 {
@@ -82,14 +84,17 @@ fn strip_query(path: String) -> String {
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")
@@ -130,6 +135,7 @@ fn route_health() -> String {
+ ",\"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
@@ -147,6 +153,7 @@ fn route_lineage() -> String {
return raw
}
@manager
fn route_imprint_contextual(body: String) -> String {
if str_eq(body, "") {
return "{\"ok\":false,\"error\":\"empty body\"}"
@@ -169,6 +176,7 @@ fn route_imprint_contextual(body: String) -> String {
return "{\"ok\":true,\"id\":\"" + id + "\"}"
}
@manager
fn route_imprint_user(body: String) -> String {
if str_eq(body, "") {
return "{\"ok\":false,\"error\":\"empty body\"}"
@@ -191,6 +199,7 @@ fn route_imprint_user(body: String) -> String {
return "{\"ok\":true,\"id\":\"" + id + "\"}"
}
@manager
fn route_synthesize(body: String) -> String {
if str_eq(body, "") {
return "{\"error\":\"body is required\",\"code\":\"missing_param\"}"
@@ -218,6 +227,7 @@ fn route_synthesize(body: String) -> String {
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")
@@ -300,6 +310,7 @@ fn handle_dharma_recv(body: String) -> String {
// 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, "") {
@@ -310,6 +321,7 @@ fn connectd_get(suffix: String) -> String {
// 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
@@ -323,38 +335,529 @@ fn connectd_post(suffix: String, body: String) -> String {
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")
//
// @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 {
// TODO(reliability #8): engram_save races with awareness loop mem_save().
// Both now use atomic write-to-temp+rename (el_runtime.c). Serialised
// by engram_global_mu. Future: add engram_edges_json() builtin.
let snap_path: String = env("HOME") + "/.neuron/engram/snapshot.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 }
}
// 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\"}"
}
if str_eq(clean, "/api/connectors/add") {
return connectd_post("/mcp/servers/add", body)
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)
screened_reply
}
if str_eq(clean, "/api/connectors/toggle") {
return connectd_post("/mcp/servers/toggle", body)
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\"}"
}
if str_eq(clean, "/api/connectors/auto-approve") {
return connectd_post("/mcp/servers/auto-approve", body)
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)
screened_reply
}
if str_eq(clean, "/api/connectors/remove") {
return connectd_post("/mcp/servers/remove", body)
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)
}
if str_eq(clean, "/api/connectors/secret") {
return connectd_post("/mcp/servers/secret", 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)
}
if str_eq(clean, "/api/connectors/oauth/start") {
return connectd_post("/mcp/oauth/start", body)
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)
}
// 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 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 HTTP entry point (registered via http_serve_async in soul.el).
// Rate-limits, then dispatches through the compiler-synthesized @route table.
// The sentinel maps to 404 for a recognised method or 405 for an unknown one,
// reproducing the per-method-block fallthroughs of the former dispatcher.
//
@manager
fn handle_request(method: String, path: String, body: String) -> String {
let clean: String = strip_query(path)
@@ -369,358 +872,16 @@ fn handle_request(method: String, path: String, body: String) -> String {
}
}
if str_eq(method, "POST") && str_eq(clean, "/dharma/recv") {
return handle_dharma_recv(body)
// 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
}
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") {
// TODO(reliability #8): engram_save races with awareness loop mem_save().
// Both now use atomic write-to-temp+rename (el_runtime.c). Serialised
// by engram_global_mu. Future: add engram_edges_json() builtin.
let snap_path: String = env("HOME") + "/.neuron/engram/snapshot.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 {
let screened_reply: String = layered_cycle(eff_msg)
screened_reply
}
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)
}
}
// 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)
}
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 {
let screened_reply: String = layered_cycle(raw_msg)
screened_reply
}
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)
}
+1 -1
View File
@@ -1,4 +1,5 @@
// auto-generated by elc --emit-header — do not edit
extern fn flag_true(body: String, key: String) -> Bool
extern fn rate_limit_check(ip: String, path: String) -> String
extern fn strip_query(path: String) -> String
extern fn err_404(path: String) -> String
@@ -11,5 +12,4 @@ extern fn route_synthesize(body: String) -> String
extern fn handle_dharma_recv(body: String) -> String
extern fn connectd_get(suffix: String) -> String
extern fn connectd_post(suffix: String, body: String) -> String
extern fn handle_connectors(method: String, clean: String, body: String) -> String
extern fn handle_request(method: String, path: String, body: String) -> String
+1 -1
View File
@@ -244,7 +244,7 @@ fn safety_general_hard_phrases() -> String {
}
fn safety_soft_phrases() -> String {
return "[\"stressed\",\"overwhelmed\",\"can't cope\",\"cannot cope\",\"struggling\",\"anxious\",\"anxiety\",\"depressed\",\"depression\",\"lonely\",\"isolated\",\"hopeless\",\"hopelessness\",\"exhausted\",\"burnt out\",\"burned out\",\"burnout\",\"panic\",\"panicking\",\"falling apart\",\"breaking down\",\"can't handle\",\"cannot handle\",\"losing it\",\"nothing matters\",\"don't care anymore\",\"given up\",\"giving up\",\"helpless\",\"worthless\",\"useless\",\"hate myself\",\"no one cares\",\"nobody cares\",\"no one understands\",\"nobody understands\",\"empty inside\",\"can't stop crying\",\"breaking point\",\"at my limit\",\"having a breakdown\""]"
return "[\"stressed\",\"overwhelmed\",\"can't cope\",\"cannot cope\",\"struggling\",\"anxious\",\"anxiety\",\"depressed\",\"depression\",\"lonely\",\"isolated\",\"hopeless\",\"hopelessness\",\"exhausted\",\"burnt out\",\"burned out\",\"burnout\",\"panic\",\"panicking\",\"falling apart\",\"breaking down\",\"can't handle\",\"cannot handle\",\"losing it\",\"nothing matters\",\"don't care anymore\",\"given up\",\"giving up\",\"helpless\",\"worthless\",\"useless\",\"hate myself\",\"no one cares\",\"nobody cares\",\"no one understands\",\"nobody understands\",\"empty inside\",\"can't stop crying\",\"breaking point\",\"at my limit\",\"having a breakdown\"]"
}
// ISSUE 5 TODO: phrase lists are rebuilt from JSON literals on every call.
+2 -2
View File
@@ -514,10 +514,10 @@ fn session_hist_save(session_id: String, hist: String) -> Void {
let last_role: String = json_get(last_entry, "role")
let last_content: String = json_get(last_entry, "content")
let topic_snip: String = if str_len(last_content) > 200 { str_slice(last_content, 0, 200) } else { last_content }
let safe_topic: String = str_replace(topic_snip, """, "'")
let safe_topic: String = str_replace(topic_snip, "\"", "'")
let ts_now: String = int_to_str(time_now())
let topic_content: String = "last-session-topic | ts:" + ts_now + " | session:" + session_id + " | topic:" + safe_topic
let topic_tags: String = "["last-session-topic","conv:history","Conversation","session:topic"]"
let topic_tags: String = "[\"last-session-topic\",\"conv:history\",\"Conversation\",\"session:topic\"]"
let topic_label: String = "last-session-topic:" + session_id
// Delete old last-session-topic node for this session before writing fresh
let old_topic: String = engram_search_json("last-session-topic:" + session_id, 2)