091cc1fc0e
A fresh-install (SOUL_CGI_ID=ntn-genesis) boot crashed with
"Segmentation fault: 11" right after the http server came up — a real
customer's very first boot. Backtrace:
strcmp(0x1) <- str_eq (el_runtime.c:219) <- mem_save <- awareness_run
Root cause: the el runtime's engram_save returns an Int (1 = ok, 0 =
failure), but mem_save did `str_eq(engram_save(path), "")`, treating the
return as a String. str_eq runs EL_CSTR on it, which is a raw cast:
EL_CSTR(1) = (char*)0x1. On a SUCCESSFUL save (return 1) strcmp then
dereferences 0x1 and segfaults. Genesis is the first path that both seeds
the brain AND saves it successfully on the very first awareness pass, so it
crashes there; non-genesis boots (contract gate, refusal test) don't hit a
successful early mem_save, which is why they passed. handle_api_consolidate
had the identical latent bug.
Fix: read engram_save's Int result and compare `== 0` instead of str_eq'ing
it — in mem_save (memory.el) and handle_api_consolidate (neuron-api.el).
Regression: pre-existing, NOT introduced by the immutability/floor rebuild.
The pre-immutability build (1442ce2) genesis-crashes identically in the same
unchanged mem_save; #159 never actually fixed #150 for a release-runtime
build.
Regenerated dist/soul.c + per-module dist/{memory,neuron-api}.c (3GB RSS
watchdog, built against release el_runtime v1.0.0-20260501). Verified:
genesis boot survives (/health 200, no segfault), verify-soul-contract.sh
GATE PASS (PRESENCE + IMMUTABILITY), and the bounded-persona floor is still
compiled in (BOUNDED PERSONA / SOUL_PERSONA_NAME strings present).
750 lines
39 KiB
EmacsLisp
750 lines
39 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)
|
|
// engram_get_node_json returns "{}" (empty object) when node is not found — not "" or "null".
|
|
// Check all three to guard against any runtime variation.
|
|
return !str_eq(node, "") && !str_eq(node, "null") && !str_eq(node, "{}")
|
|
}
|
|
|
|
// 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 + "\"}"
|
|
}
|
|
|
|
// ── Immutability: tombstone instead of hard-delete ────────────────────────────
|
|
//
|
|
// Day-one rule: engram nodes are immutable. A "delete" must never engram_forget
|
|
// (which frees the node and drops its incident edges). Instead we TOMBSTONE: the
|
|
// original node and all its edges are KEPT and stay traversable; a small
|
|
// Tombstone marker node records the deletion (content = target id, label
|
|
// "tombstone:<id>"), wired to the target with a "tombstones" edge. Default
|
|
// bounded list reads hide tombstoned nodes (memory_hide_tombstoned); internal
|
|
// cognition and explicit ?include_deleted reads still see them.
|
|
fn tombstone_node(id: String) -> String {
|
|
// Delegates to the canonical helper in memory.el (single source of truth).
|
|
return mem_tombstone(id)
|
|
}
|
|
|
|
// tombstoned_id_set — delimited "|id1|id2|" of every tombstoned target id.
|
|
// Empty string when nothing is tombstoned (callers fast-path on that).
|
|
fn tombstoned_id_set() -> String {
|
|
let markers: String = engram_scan_nodes_by_type_json("Tombstone", 5000, 0)
|
|
if str_eq(markers, "") || str_eq(markers, "[]") { return "" }
|
|
let n: Int = json_array_len(markers)
|
|
let acc: String = "|"
|
|
let i: Int = 0
|
|
while i < n {
|
|
let m: String = json_array_get(markers, i)
|
|
let tid: String = json_get(m, "content")
|
|
let acc = if str_eq(tid, "") { acc } else { acc + tid + "|" }
|
|
let i = i + 1
|
|
}
|
|
return acc
|
|
}
|
|
|
|
// memory_hide_tombstoned — drop tombstone markers and tombstoned nodes from a
|
|
// scanned node array. BOUNDED use only (typed/paginated lists), NOT the full
|
|
// graph scan: json_array_get is O(index), so a full pass is O(n^2). Safe for the
|
|
// ~50-item memory list; a hard cap protects against a large limit. The full
|
|
// /api/graph/nodes hide needs a runtime scan filter and is deferred (see PR).
|
|
// ?include_deleted bypasses the filter (explicit traversal).
|
|
fn memory_hide_tombstoned(raw: String, path: String) -> String {
|
|
if str_contains(path, "include_deleted") { return raw }
|
|
if str_eq(raw, "") || str_eq(raw, "[]") { return raw }
|
|
let dead: String = tombstoned_id_set()
|
|
if str_eq(dead, "") { return raw }
|
|
let n: Int = json_array_len(raw)
|
|
if n > 1000 { return raw }
|
|
let out: String = "["
|
|
let first: Bool = true
|
|
let i: Int = 0
|
|
while i < n {
|
|
let node: String = json_array_get(raw, i)
|
|
let nid: String = json_get(node, "id")
|
|
let ntype: String = json_get(node, "node_type")
|
|
let is_dead: Bool = !str_eq(nid, "") && str_contains(dead, "|" + nid + "|")
|
|
let keep: Bool = !str_eq(ntype, "Tombstone") && !is_dead
|
|
let out = if keep { if first { out + node } else { out + "," + node } } else { out }
|
|
let first = if keep { false } else { first }
|
|
let i = i + 1
|
|
}
|
|
return out + "]"
|
|
}
|
|
|
|
// ── 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 — TOMBSTONE a node by id (immutable delete).
|
|
// Backs /api/neuron/node/delete and the /api/neuron/memory/delete alias the UI calls.
|
|
// The node and all its incident edges are KEPT; a Tombstone marker records the
|
|
// deletion. Never engram_forget — engram nodes are immutable by design.
|
|
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") }
|
|
if is_protected_node(id) { return api_err_protected(id) }
|
|
let existing: String = engram_get_node_json(id)
|
|
if str_eq(existing, "{}") { return api_err("node not found: " + id) }
|
|
let marker: String = tombstone_node(id)
|
|
if str_eq(marker, "") { return api_err("tombstone failed: " + id) }
|
|
return "{\"ok\":true,\"id\":\"" + id + "\",\"tombstoned\":true}"
|
|
}
|
|
|
|
// handle_api_node_update — update a node's content/fields. There is no in-place
|
|
// engram update builtin, so this creates a new node with merged fields and wires
|
|
// a "supersedes" edge new->old. The original is KEPT (immutable); the id changes,
|
|
// and the response returns the new id and the superseded id so callers re-point.
|
|
// Mirrors handle_api_memory_update / evolve exactly. Never engram_forget.
|
|
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_connect(new_id, id, el_from_float(0.9), "supersedes")
|
|
return "{\"id\":\"" + new_id + "\",\"supersedes\":\"" + 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)
|
|
if !api_persisted(id) { return api_not_persisted(id) }
|
|
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)
|
|
if !api_persisted(id) { return api_not_persisted(id) }
|
|
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 — TOMBSTONE a node by ID (immutable; mem_forget now
|
|
// tombstones). The node + edges are kept and recoverable. 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 + "\",\"tombstoned\":true}"
|
|
}
|
|
|
|
// 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":"..."}.
|
|
// Immutable delete: TOMBSTONE via tombstone_node — the node and all its incident
|
|
// edges are KEPT and stay traversable; a Tombstone marker records the deletion
|
|
// and default bounded list reads hide it. Never engram_forget. Existence is
|
|
// checked first so a bad id errors rather than faking 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) }
|
|
// Immutable delete: tombstone, never mem_forget/engram_forget. Node + edges KEPT.
|
|
let marker: String = tombstone_node(node_id)
|
|
if str_eq(marker, "") { return api_err("tombstone failed: " + node_id) }
|
|
return "{\"ok\":true,\"id\":\"" + node_id + "\",\"tombstoned\":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") }
|
|
// Immutable: mem_forget now tombstones (keep node + edges), never hard-delete.
|
|
mem_forget(node_id)
|
|
return "{\"ok\":true,\"id\":\"" + node_id + "\",\"tombstoned\":true,\"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)
|
|
let raw: String = api_or_empty(engram_scan_nodes_by_type_json(node_type, limit, 0))
|
|
// Hide tombstoned nodes from the default (bounded) memory list.
|
|
// ?include_deleted=1 returns them for explicit traversal.
|
|
return memory_hide_tombstoned(raw, path)
|
|
}
|
|
|
|
// ── 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 returns an Int (1 = ok, 0 = failure); str_eq on it derefs
|
|
// EL_CSTR(1)=0x1 and SIGSEGVs on success (issue #150). Check the Int.
|
|
let saved: Int = engram_save(snap)
|
|
if saved == 0 {
|
|
println("[api] consolidate: engram_save failed for " + snap + " — snapshot may be out of sync")
|
|
}
|
|
}
|
|
if !str_eq(summary, "") {
|
|
let safe_summary: String = str_replace(summary, "\"", "'")
|
|
let tags: String = "[\"SessionSummary\",\"consolidate\"]"
|
|
let summary_id: 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
|
|
)
|
|
if str_eq(summary_id, "") {
|
|
println("[api] consolidate: session summary engram write failed — summary node lost")
|
|
}
|
|
}
|
|
return "{\"ok\":true,\"snapshot\":\"" + snap + "\"}"
|
|
}
|