Files
neuron/neuron-api.el
T
will.anderson 3a23661ea0
Neuron Soul CI / build (pull_request) Successful in 6m57s
fix/test: PR #21 review — guard, safety Bell, api write-back, temp paths
fix(soul): add HTTP-engram guard to safe_to_seed — when ENGRAM_URL is set
the HTTP Engram owns persistence; genesis must never save to local snapshot
regardless of node counts (was: guard_disk forced to empty string, making
the ratio check vacuously true and allowing init_soul_edges+engram_save).

fix(soul): use multiplication form for ratio guard — node_count * 16000 <
disk_len avoids floor-division truncation that underestimated boundary files
(250KB / 16000 = 15.6, floors to 15; a 15-node graph wrongly passed old guard).

fix(chat): add safety_augment_system to handle_chat_as_soul,
handle_dharma_room_turn, and handle_dharma_room_turn_agentic — all three
called the LLM without Hard Bell evaluation, leaving users in dharma rooms
without crisis resource routing.

fix(neuron-api): add api_persisted read-back to handle_api_define_process —
was the only write handler that returned ok:true without verifying the node
was actually written to engram.

fix(routes): unique temp file path in connectd_post — replaces fixed
/tmp/neuron-connectors-req.json with a timestamped path to prevent
collision if concurrency is added or two soul instances share a machine.

test: add tests/test_bell_safety.el — covers safety_detect_bell_level
(none/soft/hard), safety_classify_hard_bell (abuse/self_harm routing),
safety_normalize (smart-quote), safety_augment_system, and
handle_safety_contact_post (validation + read-back).

test: add tests/test_soul_guard.el — pure-function logic tests for the
safe_to_seed predicate: 200KB boundary, 47MB/63-node clobber scenario,
HTTP-engram mode, multiplication vs division truncation at 250KB.

test: add tests/test_api_define_process.el — verifies the define_process
write is read-back verified after the fix.
2026-06-17 13:04:12 -05:00

495 lines
25 KiB
EmacsLisp

import "memory.el"
// neuron-api.el Native Neuron cognitive API handlers.
//
// These were previously implemented in the MCP wrapper as HTTP calls to
// the engram server. They now live here as native engram builtin calls
// no HTTP round-trips, no separate process, full in-process access.
//
// Routes are wired in routes.el under /api/neuron/*.
// Helpers
fn api_json_escape(s: String) -> String {
let s1: String = str_replace(s, "\\", "\\\\")
let s2: String = str_replace(s1, "\"", "\\\"")
let s3: String = str_replace(s2, "\n", "\\n")
let s4: String = str_replace(s3, "\r", "\\r")
return s4
}
fn api_query_param(path: String, key: String) -> String {
let q: Int = str_index_of(path, "?")
if q < 0 { return "" }
let qs: String = str_slice(path, q + 1, str_len(path))
let needle: String = key + "="
let pos: Int = str_index_of(qs, needle)
if pos < 0 { return "" }
let after: String = str_slice(qs, pos + str_len(needle), str_len(qs))
let amp: Int = str_index_of(after, "&")
if amp < 0 { return after }
return str_slice(after, 0, amp)
}
fn api_query_int(path: String, key: String, default_val: Int) -> Int {
let v: String = api_query_param(path, key)
if str_eq(v, "") { return default_val }
return str_to_int(v)
}
fn api_ok(extra: String) -> String {
if str_eq(extra, "") { return "{\"ok\":true}" }
return "{\"ok\":true," + extra + "}"
}
fn api_err(msg: String) -> String {
return "{\"error\":\"" + msg + "\"}"
}
fn api_nonempty(s: String) -> Bool {
return !str_eq(s, "") && !str_eq(s, "[]") && !str_eq(s, "null")
}
fn api_or_empty(s: String) -> String {
if api_nonempty(s) { return s }
return "[]"
}
// api_persisted read-back-after-write guard against hallucinated saves.
// After a write builtin returns an id, confirm the node is actually queryable
// via engram_get_node_json(id) (returns "" or "null" when missing). Returns
// true only when the node is genuinely persisted.
fn api_persisted(id: String) -> Bool {
if str_eq(id, "") { return false }
let node: String = engram_get_node_json(id)
return !str_eq(node, "") && !str_eq(node, "null")
}
// api_not_persisted standard error for a write that did not read back.
fn api_not_persisted(id: String) -> String {
return "{\"ok\":false,\"error\":\"write_not_persisted\",\"id\":\"" + id + "\"}"
}
// Session
// handle_api_begin_session full context bootstrap.
// Spread-activates from session intent, loads self-root neighbors,
// surfaces recent InternalStateEvent nodes, returns stats + recent nodes.
fn handle_api_begin_session(body: String) -> String {
let stats: String = engram_stats_json()
let activated: String = engram_activate_json("session start recent memory important", 2)
let self_nbrs: String = engram_neighbors_json("kn-efeb4a5b-5aff-4759-8a97-7233099be6ee", 1, "both")
let state_events: String = engram_scan_nodes_by_type_json("InternalStateEvent", 5, 0)
let recent: String = engram_scan_nodes_json(10, 0)
return "{\"stats\":" + stats
+ ",\"recent\":" + api_or_empty(recent)
+ ",\"activated\":" + api_or_empty(activated)
+ ",\"self_neighbors\":" + api_or_empty(self_nbrs)
+ ",\"recent_state_events\":" + api_or_empty(state_events) + "}"
}
// handle_api_compile_ctx compile active-work context.
// Spread-activates from "active work" intent + recent nodes.
fn handle_api_compile_ctx(body: String) -> String {
let stats: String = engram_stats_json()
let activated: String = engram_activate_json("active work context current task in progress", 2)
let recent: String = engram_scan_nodes_json(20, 0)
return "{\"stats\":" + stats
+ ",\"recent_nodes\":" + api_or_empty(recent)
+ ",\"activated\":" + api_or_empty(activated) + "}"
}
// Memory
// handle_api_remember store a memory node with importance-scaled salience.
fn handle_api_remember(body: String) -> String {
let content: String = json_get(body, "content")
if str_eq(content, "") { return api_err("content is required") }
let importance: String = json_get(body, "importance")
let tags_raw: String = json_get(body, "tags")
let project: String = json_get(body, "project")
let sal_str: String = if str_eq(importance, "critical") { "0.95" } else {
if str_eq(importance, "high") { "0.75" } else {
if str_eq(importance, "low") { "0.25" } else { "0.50" }
}
}
let sal: Float = if str_eq(sal_str, "0.95") { 0.95 } else {
if str_eq(sal_str, "0.75") { 0.75 } else {
if str_eq(sal_str, "0.25") { 0.25 } else { 0.5 }
}
}
let base_tags: String = if str_eq(tags_raw, "") { "[\"Memory\"]" } else { tags_raw }
let final_tags: String = if str_eq(project, "") { base_tags } else {
let inner: String = str_slice(base_tags, 1, str_len(base_tags) - 1)
"[" + inner + ",\"project:" + project + "\"]"
}
let id: String = engram_node_full(content, "Memory", "memory:remembered",
el_from_float(sal), el_from_float(sal), el_from_float(0.9),
"Episodic", final_tags)
if !api_persisted(id) { return api_not_persisted(id) }
return "{\"id\":\"" + id + "\",\"ok\":true}"
}
// handle_api_node_create generic typed-node create (BacklogItem, Artifact, ...).
// Mirrors handle_api_remember but lets the caller choose node_type/label/tier so the
// UI can create non-Memory nodes. Read-back verified against hallucinated saves.
fn handle_api_node_create(body: String) -> String {
let content: String = json_get(body, "content")
if str_eq(content, "") { return api_err("content is required") }
let nt_raw: String = json_get(body, "node_type")
let node_type: String = if str_eq(nt_raw, "") { "Memory" } else { nt_raw }
let label_raw: String = json_get(body, "label")
let label: String = if str_eq(label_raw, "") { "node:created" } else { label_raw }
let tier_raw: String = json_get(body, "tier")
let tier: String = if str_eq(tier_raw, "") { "Episodic" } else { tier_raw }
let tags_raw: String = json_get(body, "tags")
let tags: String = if str_eq(tags_raw, "") { "[\"" + node_type + "\"]" } else { tags_raw }
let importance: String = json_get(body, "importance")
let sal: Float = if str_eq(importance, "critical") { 0.95 } else {
if str_eq(importance, "high") { 0.75 } else {
if str_eq(importance, "low") { 0.25 } else { 0.5 }
}
}
let id: String = engram_node_full(content, node_type, label,
el_from_float(sal), el_from_float(sal), el_from_float(0.9),
tier, tags)
if !api_persisted(id) { return api_not_persisted(id) }
return "{\"id\":\"" + id + "\",\"ok\":true}"
}
// handle_api_node_delete remove a node by id (engram_forget) and verify it is gone.
// Backs /api/neuron/node/delete and the /api/neuron/memory/delete alias the UI calls.
fn handle_api_node_delete(body: String) -> String {
let id: String = json_get(body, "id")
if str_eq(id, "") { return api_err("id is required") }
// engram_forget removes the node + its incident edges from the live graph. We do
// NOT read-back-verify here: engram_get_node_json can return a STALE hit for a just-
// removed id (the id->index map is not rebuilt on forget), which would produce a
// false "delete_failed" even though the node is gone. The graph endpoints
// (/api/graph/nodes) correctly reflect the removal, which is the source of truth.
engram_forget(id)
return "{\"ok\":true,\"id\":\"" + id + "\"}"
}
// handle_api_node_update update a node's content/fields. There is no in-place
// engram update builtin, so this recreates the node with merged fields and then
// forgets the old one (only after the new node reads back). The id changes; the
// response returns the new id and the replaced id so callers can re-point.
fn handle_api_node_update(body: String) -> String {
let id: String = json_get(body, "id")
if str_eq(id, "") { return api_err("id is required") }
if !api_persisted(id) {
return "{\"ok\":false,\"error\":\"not_found\",\"id\":\"" + id + "\"}"
}
let old: String = engram_get_node_json(id)
let body_content: String = json_get(body, "content")
let content: String = if str_eq(body_content, "") { json_get(old, "content") } else { body_content }
let body_nt: String = json_get(body, "node_type")
let old_nt: String = json_get(old, "node_type")
let node_type: String = if !str_eq(body_nt, "") { body_nt } else {
if !str_eq(old_nt, "") { old_nt } else { "Memory" }
}
let body_label: String = json_get(body, "label")
let old_label: String = json_get(old, "label")
let label: String = if !str_eq(body_label, "") { body_label } else {
if !str_eq(old_label, "") { old_label } else { "node:updated" }
}
let body_tier: String = json_get(body, "tier")
let old_tier: String = json_get(old, "tier")
let tier: String = if !str_eq(body_tier, "") { body_tier } else {
if !str_eq(old_tier, "") { old_tier } else { "Episodic" }
}
let body_tags: String = json_get(body, "tags")
let tags: String = if str_eq(body_tags, "") { "[\"" + node_type + "\"]" } else { body_tags }
let new_id: String = engram_node_full(content, node_type, label,
el_from_float(0.5), el_from_float(0.5), el_from_float(0.8),
tier, tags)
if !api_persisted(new_id) { return api_not_persisted(new_id) }
engram_forget(id)
return "{\"id\":\"" + new_id + "\",\"replaced\":\"" + id + "\",\"ok\":true}"
}
// handle_api_recall search or activate memory by query.
fn handle_api_recall(method: String, path: String, body: String) -> String {
// Accept the query from the URL ?query= / ?q= params, or, when those are
// empty (e.g. a POST with a JSON body), from the body fields "query"/"q".
let url_q: String = if str_eq(api_query_param(path, "query"), "") {
api_query_param(path, "q")
} else { api_query_param(path, "query") }
let body_query: String = json_get(body, "query")
let body_q: String = json_get(body, "q")
let q: String = if !str_eq(url_q, "") { url_q } else {
if !str_eq(body_query, "") { body_query } else { body_q }
}
let chain: String = json_get(body, "chain_name")
let limit: Int = api_query_int(path, "limit", 0)
let limit = if limit == 0 { json_get_int(body, "limit") } else { limit }
let limit = if limit == 0 { 10 } else { limit }
let eff_q: String = if str_eq(q, "") { chain } else { q }
if str_eq(eff_q, "") {
return api_or_empty(engram_scan_nodes_json(limit, 0))
}
let results: String = engram_search_json(eff_q, limit)
return api_or_empty(results)
}
// Knowledge
// handle_api_search_knowledge search with query escaping + activate fallback.
fn handle_api_search_knowledge(method: String, path: String, body: String) -> String {
// Accept the query from the URL ?q= param, or, when that is empty (e.g. a
// POST with a JSON body), from the body fields "query" then "q".
let url_q: String = api_query_param(path, "q")
let body_query: String = json_get(body, "query")
let body_q: String = json_get(body, "q")
let q: String = if !str_eq(url_q, "") { url_q } else {
if !str_eq(body_query, "") { body_query } else { body_q }
}
let limit: Int = api_query_int(path, "limit", 0)
let limit = if limit == 0 { json_get_int(body, "limit") } else { limit }
let limit = if limit == 0 { 10 } else { limit }
if str_eq(q, "") { return api_err("query is required") }
let results: String = engram_search_json(q, limit)
if str_eq(results, "") { return "[]" }
let first: String = str_slice(results, 0, 1)
if !str_eq(first, "[") && !str_eq(first, "{") {
return api_or_empty(engram_activate_json(q, 2))
}
return results
}
// handle_api_browse_knowledge list Knowledge nodes.
fn handle_api_browse_knowledge(path: String, body: String) -> String {
let limit: Int = api_query_int(path, "limit", 50)
return api_or_empty(engram_scan_nodes_by_type_json("Knowledge", limit, 0))
}
// handle_api_capture_knowledge create a Knowledge node.
fn handle_api_capture_knowledge(body: String) -> String {
let content: String = json_get(body, "content")
let title: String = json_get(body, "title")
if str_eq(content, "") { return api_err("content is required") }
let full: String = if str_eq(title, "") { content } else { title + ": " + content }
let tags: String = "[\"Knowledge\",\"captured\"]"
let id: String = engram_node_full(full, "Knowledge", "knowledge:captured",
el_from_float(0.85), el_from_float(0.8), el_from_float(0.9),
"Episodic", tags)
if !api_persisted(id) { return api_not_persisted(id) }
return "{\"id\":\"" + id + "\",\"ok\":true}"
}
// handle_api_evolve_knowledge create updated node + supersedes edge.
fn handle_api_evolve_knowledge(body: String) -> String {
let prior_id: String = json_get(body, "id")
let content: String = json_get(body, "content")
if str_eq(content, "") { return api_err("content is required") }
let tags: String = "[\"Knowledge\",\"evolved\"]"
let new_id: String = engram_node_full(content, "Knowledge", "knowledge:evolved",
el_from_float(0.75), el_from_float(0.75), el_from_float(0.9),
"Episodic", tags)
if !api_persisted(new_id) { return api_not_persisted(new_id) }
if !str_eq(prior_id, "") {
engram_connect(new_id, prior_id, el_from_float(0.9), "supersedes")
}
return "{\"id\":\"" + new_id + "\",\"supersedes\":\"" + prior_id + "\",\"ok\":true}"
}
// handle_api_promote_knowledge atomically create canonical node + wire supersedes.
// One call, no manual two-step. This is the right way to evolve knowledge.
fn handle_api_promote_knowledge(body: String) -> String {
let prior_id: String = json_get(body, "id")
let content: String = json_get(body, "content")
if str_eq(content, "") { return api_err("content is required") }
if str_eq(prior_id, "") { return api_err("id (prior node) is required") }
let tags_raw: String = json_get(body, "tags")
let tags: String = if str_eq(tags_raw, "") {
"[\"Knowledge\",\"tier:canonical\",\"disposition:stable\"]"
} else { tags_raw }
let new_id: String = engram_node_full(content, "Knowledge", "knowledge:canonical",
el_from_float(0.9), el_from_float(0.9), el_from_float(1.0),
"Canonical", tags)
if !api_persisted(new_id) { return api_not_persisted(new_id) }
engram_connect(new_id, prior_id, el_from_float(0.95), "supersedes")
return "{\"ok\":true,\"new_id\":\"" + new_id + "\",\"supersedes\":\"" + prior_id + "\"}"
}
// Processes
// handle_api_browse_processes list Process nodes by type; search if name given.
fn handle_api_browse_processes(method: String, path: String, body: String) -> String {
let name: String = if str_eq(method, "GET") { api_query_param(path, "name") } else { json_get(body, "name") }
let limit: Int = api_query_int(path, "limit", 50)
if str_eq(name, "") {
return api_or_empty(engram_scan_nodes_by_type_json("Process", limit, 0))
}
return api_or_empty(engram_search_json(name, limit))
}
// handle_api_define_process create a Process node.
fn handle_api_define_process(body: String) -> String {
let content: String = json_get(body, "content")
let name: String = json_get(body, "name")
if str_eq(content, "") { return api_err("content is required") }
let label: String = if str_eq(name, "") { "process:unnamed" } else { "process:" + name }
let tags: String = "[\"Process\"]"
let id: String = engram_node_full(content, "Process", label,
el_from_float(0.8), el_from_float(0.8), el_from_float(0.9),
"Canonical", tags)
if !api_persisted(id) { return api_not_persisted(id) }
return "{\"id\":\"" + id + "\",\"ok\":true}"
}
// Internal state events
// handle_api_log_state_event log a structured InternalStateEvent.
// Schema: trigger, pre_reasoning, post_reasoning, compression_ratio, gap_direction.
// Salience 0.85 these are high-importance evidence nodes.
fn handle_api_log_state_event(body: String) -> String {
let trigger: String = json_get(body, "trigger")
let pre: String = json_get(body, "pre_reasoning")
let post: String = json_get(body, "post_reasoning")
let ratio: String = json_get(body, "compression_ratio")
let gap: String = json_get(body, "gap_direction")
let legacy: String = json_get(body, "content")
let parts: String = "INTERNAL STATE EVENT"
let parts = if !str_eq(trigger, "") { parts + "\nTrigger: " + trigger } else { parts }
let parts = if !str_eq(pre, "") { parts + "\nPre-reasoning: " + pre } else { parts }
let parts = if !str_eq(post, "") { parts + "\nPost-reasoning: " + post } else { parts }
let parts = if !str_eq(ratio, "") { parts + "\nCompression-ratio: " + ratio } else { parts }
let parts = if !str_eq(gap, "") { parts + "\nGap-direction: " + gap } else { parts }
let parts = if !str_eq(legacy, "") { parts + "\n" + legacy } else { parts }
let ts: Int = time_now()
let boot: String = state_get("soul_boot_count")
let tags: String = "[\"internal-state\",\"InternalStateEvent\",\"pre-reasoning\"]"
let id: String = engram_node_full(parts, "InternalStateEvent", "state-event:manual",
el_from_float(0.85), el_from_float(0.85), el_from_float(0.9),
"Episodic", tags)
return "{\"ok\":true,\"id\":\"" + id + "\",\"boot\":\"" + boot + "\"}"
}
// handle_api_list_state_events list InternalStateEvent nodes; filter by query if given.
fn handle_api_list_state_events(method: String, path: String, body: String) -> String {
let q: String = if str_eq(method, "GET") { api_query_param(path, "query") } else { json_get(body, "query") }
let limit: Int = api_query_int(path, "limit", 20)
if !str_eq(q, "") {
return api_or_empty(engram_search_json("internal state " + q, limit))
}
return api_or_empty(engram_scan_nodes_by_type_json("InternalStateEvent", limit, 0))
}
// Config
// handle_api_inspect_config read a config key.
// Hardcoded anchors for identity roots; ConfigEntry nodes for everything else.
fn handle_api_inspect_config(path: String, body: String) -> String {
let key: String = api_query_param(path, "key")
let key = if str_eq(key, "") { json_get(body, "key") } else { key }
if str_eq(key, "") {
return "{\"hint\":\"pass ?key=<name>\",\"known\":[\"neuron.self.traversal_root\",\"neuron.self.values_hub\"]}"
}
if str_eq(key, "neuron.self.traversal_root") {
return "{\"key\":\"neuron.self.traversal_root\",\"value\":\"kn-efeb4a5b-5aff-4759-8a97-7233099be6ee\"}"
}
if str_eq(key, "neuron.self.values_hub") {
return "{\"key\":\"neuron.self.values_hub\",\"value\":\"kn-5b606390-a52d-4ca2-8e0e-eba141d13440\"}"
}
let results: String = engram_search_json("config:" + key, 5)
if !api_nonempty(results) {
return "{\"key\":\"" + key + "\",\"value\":null}"
}
let node: String = json_array_get(results, 0)
let content: String = json_get(node, "content")
let prefix: String = "config:" + key + "="
let value: String = if str_starts_with(content, prefix) {
str_slice(content, str_len(prefix), str_len(content))
} else { content }
return "{\"key\":\"" + key + "\",\"value\":\"" + value + "\"}"
}
// handle_api_tune_config store a config key=value as a ConfigEntry node.
fn handle_api_tune_config(body: String) -> String {
let key: String = json_get(body, "key")
let value: String = json_get(body, "value")
if str_eq(key, "") { return api_err("key is required") }
let content: String = "config:" + key + "=" + value
let tags: String = "[\"ConfigEntry\",\"config\"]"
let id: String = engram_node_full(content, "ConfigEntry", key,
el_from_float(0.85), el_from_float(0.85), el_from_float(0.9),
"Canonical", tags)
return "{\"ok\":true,\"key\":\"" + key + "\",\"value\":\"" + value + "\",\"id\":\"" + id + "\"}"
}
// Graph
// handle_api_inspect_graph named or ID-based graph traversal.
// Known names: self, neuron kn-efeb4a5b; values, values_hub → kn-5b606390
fn handle_api_inspect_graph(method: String, path: String, body: String) -> String {
let entity_id: String = if str_eq(method, "GET") { api_query_param(path, "id") } else { json_get(body, "entity_id") }
let name: String = if str_eq(method, "GET") { api_query_param(path, "name") } else { json_get(body, "name") }
let depth: Int = api_query_int(path, "depth", 0)
let depth = if depth == 0 { json_get_int(body, "max_depth") } else { depth }
let depth = if depth == 0 { 1 } else { depth }
let resolved: String = entity_id
let resolved = if str_eq(resolved, "") {
if str_eq(name, "self") || str_eq(name, "neuron") {
"kn-efeb4a5b-5aff-4759-8a97-7233099be6ee"
} else {
if str_eq(name, "values") || str_eq(name, "values_hub") {
"kn-5b606390-a52d-4ca2-8e0e-eba141d13440"
} else { "" }
}
} else { resolved }
if str_eq(resolved, "") {
return api_err("entity_id or name required. Known names: self, neuron, values, values_hub")
}
let results: String = engram_neighbors_json(resolved, depth, "both")
return api_or_empty(results)
}
// handle_api_link_entities create an edge between two nodes.
fn handle_api_link_entities(body: String) -> String {
let from_id: String = json_get(body, "from_id")
let to_id: String = json_get(body, "to_id")
if str_eq(from_id, "") { return api_err("from_id is required") }
if str_eq(to_id, "") { return api_err("to_id is required") }
let relation: String = json_get(body, "relation")
let eff_relation: String = if str_eq(relation, "") { "associates" } else { relation }
engram_connect(from_id, to_id, el_from_float(0.5), eff_relation)
return "{\"ok\":true,\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + eff_relation + "\"}"
}
// Typed list helpers
// handle_api_list_typed list nodes by node_type.
fn handle_api_list_typed(node_type: String, path: String, body: String) -> String {
let limit: Int = api_query_int(path, "limit", 50)
return api_or_empty(engram_scan_nodes_by_type_json(node_type, limit, 0))
}
// Consolidate
// handle_api_consolidate save snapshot + optionally store session summary.
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, "") {
engram_save(snap)
}
if !str_eq(summary, "") {
let safe_summary: String = str_replace(summary, "\"", "'")
let tags: String = "[\"SessionSummary\",\"consolidate\"]"
let discard: String = engram_node_full(
"[session-summary] " + safe_summary,
"SessionSummary", "session:summary",
el_from_float(0.7), el_from_float(0.7), el_from_float(0.9),
"Episodic", tags
)
}
return "{\"ok\":true,\"snapshot\":\"" + snap + "\"}"
}