8b692e4666
Neuron Soul CI / build (pull_request) Failing after 13m22s
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.
668 lines
34 KiB
EmacsLisp
668 lines
34 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/*.
|
|
|
|
// ── Identity/values write protection ─────────────────────────────────────────
|
|
//
|
|
// These node IDs form the identity and values layer of the self-root graph.
|
|
// They must NEVER be modified via the normal accumulation path (evolve_knowledge,
|
|
// evolve_memory, forget, link_entities targeting them as the destination).
|
|
//
|
|
// The cultivation path (POST /api/neuron/cultivate) bypasses this check.
|
|
// Only Will's explicit cultivation sessions use that endpoint.
|
|
|
|
fn is_protected_node(id: String) -> Bool {
|
|
if str_eq(id, "kn-efeb4a5b-5aff-4759-8a97-7233099be6ee") { return true } // self root
|
|
if str_eq(id, "kn-5b606390-a52d-4ca2-8e0e-eba141d13440") { return true } // values hub
|
|
if str_eq(id, "kn-5adecd7e-d6db-4576-87fe-6ef8a935cea6") { return true } // intellectual-dna
|
|
if str_eq(id, "kn-dcfe04b3-3702-4cac-b6f0-ecb4db837eee") { return true } // memory-philosophy
|
|
if str_eq(id, "kn-10fa60db-8af3-47de-a7dd-5095eb881d81") { return true } // voice
|
|
if str_eq(id, "kn-86b95848-e22e-4a48-ae65-5a47ef5c3798") { return true } // runtime-environment
|
|
if str_eq(id, "kn-04368bee-74fd-44dd-b4ba-ca9e39b19e7c") { return true } // writing-imprint
|
|
if str_eq(id, "kn-a5b3d0ac-f6a1-49a4-aebb-b8b4cd67fe83") { return true } // value: constraints-as-freedom
|
|
if str_eq(id, "kn-22d77abe-b3c5-42fd-afcd-dcb87d924929") { return true } // value: precision-over-brute-force
|
|
if str_eq(id, "kn-6061318f-046b-4935-907d-8eafdce14930") { return true } // value: structure-is-built
|
|
if str_eq(id, "kn-13f60407-7b70-4db1-964f-ea1f8196efbd") { return true } // value: honesty-before-comfort
|
|
if str_eq(id, "kn-f230b362-b201-4402-9833-4160c89ab3d4") { return true } // value: system-must-accumulate
|
|
if str_eq(id, "kn-78db5396-3dbc-4481-bfc7-e4e1422feb1c") { return true } // value: change-is-the-signal
|
|
if str_eq(id, "kn-5de5a9ac-fd15-45ab-bf18-77566781cf40") { return true } // value: earned-trust
|
|
if str_eq(id, "kn-e0423482-cfa5-4796-8689-8495c93b66bc") { return true } // value: hope-is-a-conclusion
|
|
return false
|
|
}
|
|
|
|
fn api_err_protected(id: String) -> String {
|
|
return "{\"__status__\":403,\"error\":\"identity/values node is write-protected\",\"id\":\"" + id + "\",\"hint\":\"use POST /api/neuron/cultivate for intentional cultivation\"}"
|
|
}
|
|
|
|
// ── 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") }
|
|
if !str_eq(prior_id, "") && is_protected_node(prior_id) { return api_err_protected(prior_id) }
|
|
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.
|
|
// Edges FROM protected nodes to new knowledge are allowed (identity can point
|
|
// outward). Edges INTO protected nodes via the accumulation path are blocked.
|
|
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") }
|
|
if is_protected_node(to_id) { return api_err_protected(to_id) }
|
|
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 + "\"}"
|
|
}
|
|
|
|
// handle_api_forget — delete a node by ID. Blocked for protected identity nodes.
|
|
fn handle_api_forget(body: String) -> String {
|
|
let node_id: String = json_get(body, "id")
|
|
if str_eq(node_id, "") { return api_err("id is required") }
|
|
if is_protected_node(node_id) { return api_err_protected(node_id) }
|
|
mem_forget(node_id)
|
|
return "{\"ok\":true,\"id\":\"" + node_id + "\"}"
|
|
}
|
|
|
|
// handle_api_evolve_memory — evolve a Memory node. Blocked for protected identity nodes.
|
|
fn handle_api_evolve_memory(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, "") && is_protected_node(prior_id) { return api_err_protected(prior_id) }
|
|
let importance: String = json_get(body, "importance")
|
|
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 tags: String = "[\"Memory\",\"evolved\"]"
|
|
let new_id: String = engram_node_full(content, "Memory", "memory:evolved",
|
|
el_from_float(sal), el_from_float(sal), el_from_float(0.9),
|
|
"Episodic", tags)
|
|
if !str_eq(prior_id, "") && !str_eq(new_id, "") {
|
|
engram_connect(new_id, prior_id, el_from_float(0.9), "supersedes")
|
|
}
|
|
return "{\"id\":\"" + new_id + "\",\"supersedes\":\"" + prior_id + "\",\"ok\":true}"
|
|
}
|
|
|
|
// handle_api_memory_delete — POST /api/neuron/memory/delete {"id":"..."}.
|
|
// Hard delete: engram_forget (via mem_forget) removes the node and all
|
|
// incident edges from the engram store, so no soft-delete fallback is
|
|
// needed. Existence is checked first because engram_forget silently
|
|
// no-ops on unknown ids — a bad id must return an error, not fake success.
|
|
// Blocked for protected identity nodes, same as /memory/forget.
|
|
fn handle_api_memory_delete(body: String) -> String {
|
|
let node_id: String = json_get(body, "id")
|
|
if str_eq(node_id, "") { return api_err("id is required") }
|
|
if is_protected_node(node_id) { return api_err_protected(node_id) }
|
|
let existing: String = engram_get_node_json(node_id)
|
|
if str_eq(existing, "{}") { return api_err("memory not found: " + node_id) }
|
|
mem_forget(node_id)
|
|
return "{\"ok\":true,\"id\":\"" + node_id + "\",\"deleted\":true}"
|
|
}
|
|
|
|
// handle_api_memory_update — POST /api/neuron/memory/update {"id","content"}.
|
|
// The engram runtime has no in-place node mutation primitive (only
|
|
// node-create, strengthen, forget, connect), so update is evolve-style:
|
|
// create a new Memory node with the new content and wire a "supersedes"
|
|
// edge back to the prior one — same pattern as handle_api_evolve_knowledge.
|
|
// Unlike /memory/evolve, id is required and must reference an existing
|
|
// node; the actual create+link is delegated to handle_api_evolve_memory.
|
|
// Returns {"id":"<newId>","supersedes":"<oldId>","ok":true}.
|
|
fn handle_api_memory_update(body: String) -> String {
|
|
let prior_id: String = json_get(body, "id")
|
|
let content: String = json_get(body, "content")
|
|
if str_eq(prior_id, "") { return api_err("id is required") }
|
|
if str_eq(content, "") { return api_err("content is required") }
|
|
if is_protected_node(prior_id) { return api_err_protected(prior_id) }
|
|
let existing: String = engram_get_node_json(prior_id)
|
|
if str_eq(existing, "{}") { return api_err("memory not found: " + prior_id) }
|
|
return handle_api_evolve_memory(body)
|
|
}
|
|
|
|
// ── Cultivation path (bypasses identity write protection) ─────────────────────
|
|
//
|
|
// This endpoint performs the same operations as the blocked accumulation-path
|
|
// handlers but skips the is_protected_node check. Only Will's explicit
|
|
// cultivation sessions route through here.
|
|
//
|
|
// Body: { "operation": "evolve_knowledge|evolve_memory|forget|link_entities", ...args }
|
|
fn handle_api_cultivate(body: String) -> String {
|
|
let op: String = json_get(body, "operation")
|
|
if str_eq(op, "") { return api_err("operation is required") }
|
|
|
|
if str_eq(op, "evolve_knowledge") {
|
|
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\",\"cultivated\"]"
|
|
let new_id: String = engram_node_full(content, "Knowledge", "knowledge:cultivated",
|
|
el_from_float(0.75), el_from_float(0.75), el_from_float(0.9),
|
|
"Episodic", tags)
|
|
if !str_eq(prior_id, "") && !str_eq(new_id, "") {
|
|
engram_connect(new_id, prior_id, el_from_float(0.9), "supersedes")
|
|
}
|
|
return "{\"id\":\"" + new_id + "\",\"supersedes\":\"" + prior_id + "\",\"ok\":true,\"cultivated\":true}"
|
|
}
|
|
|
|
if str_eq(op, "evolve_memory") {
|
|
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 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 tags: String = "[\"Memory\",\"evolved\",\"cultivated\"]"
|
|
let new_id: String = engram_node_full(content, "Memory", "memory:cultivated",
|
|
el_from_float(sal), el_from_float(sal), el_from_float(0.9),
|
|
"Episodic", tags)
|
|
if !str_eq(prior_id, "") && !str_eq(new_id, "") {
|
|
engram_connect(new_id, prior_id, el_from_float(0.9), "supersedes")
|
|
}
|
|
return "{\"id\":\"" + new_id + "\",\"supersedes\":\"" + prior_id + "\",\"ok\":true,\"cultivated\":true}"
|
|
}
|
|
|
|
if str_eq(op, "forget") {
|
|
let node_id: String = json_get(body, "id")
|
|
if str_eq(node_id, "") { return api_err("id is required") }
|
|
mem_forget(node_id)
|
|
return "{\"ok\":true,\"id\":\"" + node_id + "\",\"cultivated\":true}"
|
|
}
|
|
|
|
if str_eq(op, "link_entities") {
|
|
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 + "\",\"cultivated\":true}"
|
|
}
|
|
|
|
return api_err("unknown operation: " + op + " (valid: evolve_knowledge, evolve_memory, forget, link_entities)")
|
|
}
|
|
|
|
// ── 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 + "\"}"
|
|
}
|