b9ef66cae9
The accumulated retrieval stack (iterations 1-9) put the claim-24 semantic
leg and the claim-10 associative leg on engram_search_json — the function
~40 internal .el call sites already used as a KEYED read. Seven of those
sites delete every record that comes back ("prune all existing X nodes,
keep exactly one"): memory.el:176, sessions.el:250/268/444/523,
soul.el:359.
mem_boot_count_inc() calls engram_search_json("soul:boot_count", 50) and
engram_forget()s all 50 results. With a lexical leg that returned 1 record.
With a semantic leg it returns 50 — the 49 nearest neighbours of the STRING
"soul:boot_count" — and the soul deletes them.
MEASURED on the harness corpus, isolated, read-only, zero writes from any
caller: 234 node records destroyed in a single boot. The deletion list is
the soul's own lookup result list, in rank order. Casualties include 6
Knowledge nodes, a layer-1 "CORE IDENTITY - GENESIS, LINEAGE" Memory, the
value node kn-58874a74, and the gold answers to 8 of the 75 gold-set
queries. After the fix: 1 deletion, which is the one the code intends.
THE BOUNDARY, from Will. Claim 24 authorises the vector index "to respond
to EMBEDDING SEARCH QUERIES by returning the node records whose embedding
vectors have the highest cosine similarity to a query vector". A keyed
state read is not an embedding search query; it is the identifier-keyed
retrieval of claim 23 ("node records are stored under a key encoding the
node identifier"). One function served both, so a nearest neighbour of
"soul:boot_count" was treated as a boot counter.
So: engram_search_json returns to its lexical contract, and the legs move
to engram_recall_json, which is what /api/neuron/recall reaches — the route
the MCP wrapper, the app, and this harness all call. Retrieval quality on
that route is unchanged by construction.
MEASURED, 75-query extended gold set, embedded corpus, vs the iteration-9
baseline: +3 / -0 (q15, q28, q60), p=0.2500, hit@5 53.8 -> 58.5%, latency
1.02x, every regression guard held, nonsense 10/10. Net +3 against a floor
of 6 is NOT-SHOWN and I am not calling it an improvement. The deliverable
is the defect.
Diagnostics kept, env-gated (EG_DIAG / EG_DIAG_ID), zero cost when unset:
node/embedding census at load, per-query leg dump, and a FORGET log — the
last is the regression detector for exactly this class of bug.
LIMIT, stated: handle_api_search_knowledge still uses the lexical function.
It is a retrieval surface and arguably wants the legs, but nothing in this
harness measures it, so I did not change unmeasured behaviour.
888 lines
46 KiB
EmacsLisp
888 lines
46 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, "&")
|
|
let raw: String = if amp < 0 { after } else { str_slice(after, 0, amp) }
|
|
// URL-decode the extracted value BEFORE any downstream tokenizing. Clients
|
|
// percent-encode spaces (%20) and form-encode them as '+', so a multi-word
|
|
// query like "foo bar" arrives as "foo%20bar" / "foo+bar". Left undecoded,
|
|
// the ranked lexical search sees a single un-splittable token and matches
|
|
// nothing (single-word queries still hit). url_decode maps '+' -> space
|
|
// and %XX -> byte, restoring the word boundaries for recall + knowledge search.
|
|
return url_decode(raw)
|
|
}
|
|
|
|
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 "[]"
|
|
}
|
|
|
|
// ── Compact projection for session/context digests ────────────────────────────
|
|
//
|
|
// beginSession/compileCtx are session-INIT digests, not full graph dumps. The
|
|
// engram scan/activate builtins return FULL node objects — content runs to tens
|
|
// of KB per node (the self-identity hub is ~90KB alone), and node JSON carries
|
|
// content + metadata + timestamps. Concatenated unbounded, the assembled response
|
|
// reached ~900KB and — after the MCP wrapper re-escapes it into a stringified
|
|
// text block — the client dropped the socket ("connection closed unexpectedly")
|
|
// on every call. These helpers CAP the array length and project each node down
|
|
// to a light identity + a bounded, UTF-8-safe content snippet, holding the
|
|
// digest well under ~150KB regardless of graph size. Full content stays
|
|
// available on demand via recall / fetch / inspectGraph.
|
|
|
|
// api_num_or_zero — raw JSON numeric literal for `key`, or "0" when absent.
|
|
// Used for numeric node/activation fields so they stay unquoted (valid JSON).
|
|
fn api_num_or_zero(obj: String, key: String) -> String {
|
|
let v: String = json_get_raw(obj, key)
|
|
if str_eq(v, "") { return "0" }
|
|
return v
|
|
}
|
|
|
|
// api_utf8_trunc — byte-truncate `s` to at most `n` bytes WITHOUT splitting a
|
|
// multibyte UTF-8 sequence (str_slice is byte-based). Backs the cut off while the
|
|
// first EXCLUDED byte is a UTF-8 continuation byte (0x80..0xBF), so the snippet is
|
|
// always a valid prefix. Guards against re-introducing a parse failure via
|
|
// invalid UTF-8 in a JSON string value.
|
|
fn api_utf8_trunc(s: String, n: Int) -> String {
|
|
if str_len(s) <= n { return s }
|
|
let cut: Int = n
|
|
let scanning: Bool = true
|
|
while scanning && cut > 0 {
|
|
let b: Int = str_char_code(s, cut)
|
|
let is_cont: Bool = b >= 128 && b < 192
|
|
let cut = if is_cont { cut - 1 } else { cut }
|
|
let scanning = is_cont
|
|
}
|
|
return str_slice(s, 0, cut)
|
|
}
|
|
|
|
// api_compact_node — light projection of a full engram node: identity fields +
|
|
// a bounded, UTF-8-safe content snippet. Drops embeddings, metadata, tags, and
|
|
// timestamps; truncates content. `content_truncated` flags a clipped snippet.
|
|
fn api_compact_node(node: String, snip: Int) -> String {
|
|
let id: String = json_get(node, "id")
|
|
let ntype: String = json_get(node, "node_type")
|
|
let label: String = json_get(node, "label")
|
|
let tier: String = json_get(node, "tier")
|
|
let content: String = json_get(node, "content")
|
|
let snippet: String = api_utf8_trunc(content, snip)
|
|
let trunc_str: String = if str_len(content) > snip { "true" } else { "false" }
|
|
return "{\"id\":\"" + api_json_escape(id) + "\""
|
|
+ ",\"node_type\":\"" + api_json_escape(ntype) + "\""
|
|
+ ",\"label\":\"" + api_json_escape(label) + "\""
|
|
+ ",\"tier\":\"" + api_json_escape(tier) + "\""
|
|
+ ",\"importance\":" + api_num_or_zero(node, "importance")
|
|
+ ",\"salience\":" + api_num_or_zero(node, "salience")
|
|
+ ",\"content\":\"" + api_json_escape(snippet) + "\""
|
|
+ ",\"content_truncated\":" + trunc_str + "}"
|
|
}
|
|
|
|
// api_compact_node_array — map api_compact_node over a bare-node array, capping
|
|
// the element count. For scan results (recent, typed lists).
|
|
fn api_compact_node_array(raw: String, max_items: Int, snip: Int) -> String {
|
|
if !api_nonempty(raw) { return "[]" }
|
|
let n: Int = json_array_len(raw)
|
|
let cap: Int = if n < max_items { n } else { max_items }
|
|
let out: String = "["
|
|
let i: Int = 0
|
|
while i < cap {
|
|
let node: String = json_array_get(raw, i)
|
|
let sep: String = if i == 0 { "" } else { "," }
|
|
let out = out + sep + api_compact_node(node, snip)
|
|
let i = i + 1
|
|
}
|
|
return out + "]"
|
|
}
|
|
|
|
// api_compact_activated — like api_compact_node_array but for activation results,
|
|
// whose elements wrap the node as {"node":{...},"activation_strength":...,...}.
|
|
// Preserves the activation scalars, compacts the inner node.
|
|
fn api_compact_activated(raw: String, max_items: Int, snip: Int) -> String {
|
|
if !api_nonempty(raw) { return "[]" }
|
|
let n: Int = json_array_len(raw)
|
|
let cap: Int = if n < max_items { n } else { max_items }
|
|
let out: String = "["
|
|
let i: Int = 0
|
|
while i < cap {
|
|
let el: String = json_array_get(raw, i)
|
|
let node: String = json_get_raw(el, "node")
|
|
let sep: String = if i == 0 { "" } else { "," }
|
|
let out = out + sep + "{\"node\":" + api_compact_node(node, snip)
|
|
+ ",\"activation_strength\":" + api_num_or_zero(el, "activation_strength")
|
|
+ ",\"working_memory_weight\":" + api_num_or_zero(el, "working_memory_weight")
|
|
+ ",\"epistemic_confidence\":" + api_num_or_zero(el, "epistemic_confidence")
|
|
+ ",\"hops\":" + api_num_or_zero(el, "hops")
|
|
+ ",\"promoted\":" + api_num_or_zero(el, "promoted") + "}"
|
|
let i = i + 1
|
|
}
|
|
return out + "]"
|
|
}
|
|
|
|
// 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 {
|
|
// PAYLOAD BOUND: this handler was the highest-fanout working-set endpoint —
|
|
// a depth-2 spread PLUS the full neighbor dump of the self-identity hub
|
|
// (~90KB alone; node JSON carries full content + embeddings). On the ~12k-node
|
|
// store the assembled response ran to ~900KB, then roughly doubled through two
|
|
// rounds of JSON re-escaping in the MCP wrapper — the client saw "socket
|
|
// connection closed unexpectedly" on every beginSession call. Fix: depth-2 →
|
|
// depth-1 spread, drop the self-hub dump (identity loading has its own tool,
|
|
// inspectGraph), cap every list, and project each node to a light identity +
|
|
// a bounded, UTF-8-safe content snippet. self_neighbors kept as [] for
|
|
// response-shape compatibility. Response drops ~900KB → ~12KB; full content
|
|
// stays available on demand via recall / fetch / inspectGraph.
|
|
let stats: String = engram_stats_json()
|
|
let activated_raw: String = engram_activate_json("session start recent memory important", 1)
|
|
let activated: String = api_compact_activated(activated_raw, 8, 240)
|
|
let state_events_raw: String = engram_scan_nodes_by_type_json("InternalStateEvent", 5, 0)
|
|
let state_events: String = api_compact_node_array(state_events_raw, 5, 500)
|
|
let recent_raw: String = engram_scan_nodes_json(10, 0)
|
|
let recent: String = api_compact_node_array(recent_raw, 10, 240)
|
|
return "{\"stats\":" + stats
|
|
+ ",\"recent\":" + recent
|
|
+ ",\"activated\":" + activated
|
|
+ ",\"self_neighbors\":[]"
|
|
+ ",\"recent_state_events\":" + 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()
|
|
// PAYLOAD BOUND: same digest treatment as begin_session. This handler's
|
|
// depth-2 spread returns even more full nodes, so bounding here is essential —
|
|
// cap to 10 activated + 20 recent, project to UTF-8-safe snippets.
|
|
let activated_raw: String = engram_activate_json("active work context current task in progress", 2)
|
|
let activated: String = api_compact_activated(activated_raw, 10, 240)
|
|
let recent_raw: String = engram_scan_nodes_json(20, 0)
|
|
let recent: String = api_compact_node_array(recent_raw, 20, 240)
|
|
return "{\"stats\":" + stats
|
|
+ ",\"recent_nodes\":" + recent
|
|
+ ",\"activated\":" + 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",
|
|
sal, 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,
|
|
sal, 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))
|
|
}
|
|
// engram_recall_json, not engram_search_json: this route IS the retrieval
|
|
// surface (claim 24's "embedding search queries"), so it gets the semantic
|
|
// and associative legs. engram_search_json stays lexical because ~40
|
|
// internal call sites pass a KEY and seven of them delete every record
|
|
// that comes back — see the boundary note above eg_search_json_impl.
|
|
let results: String = engram_recall_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.
|
|
// LABEL FIX (2026-07-23 self-review): the sentinel label "knowledge:captured"
|
|
// made every capture anonymous in WM telemetry (35 identical wm_top entries)
|
|
// and starved the curiosity auto-term seeder, which needs meaningful labels.
|
|
// Use the title as the label; empty label lets engram_node_full derive
|
|
// content[:60], which for captures starts with the title anyway.
|
|
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 lbl: String = str_slice(title, 0, 80)
|
|
let tags: String = "[\"Knowledge\",\"captured\"]"
|
|
let id: String = engram_node_full(full, "Knowledge", lbl,
|
|
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\"]"
|
|
// Empty label → engram_node_full derives content[:60] (LABEL FIX 2026-07-23).
|
|
let new_id: String = engram_node_full(content, "Knowledge", "",
|
|
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 }
|
|
// Empty label → engram_node_full derives content[:60] (LABEL FIX 2026-07-23).
|
|
let new_id: String = engram_node_full(content, "Knowledge", "",
|
|
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",
|
|
sal, 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",
|
|
sal, 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 + "\"}"
|
|
}
|