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 "[]"
}

// ── 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_float_or — parse a numeric JSON field of `obj` as Float, or `dflt` when
// the field is absent. Backs neighbor relevance scoring.
fn api_float_or(obj: String, key: String, dflt: Float) -> Float {
    let v: String = json_get_raw(obj, key)
    if str_eq(v, "") { return dflt }
    return str_to_float(v)
}

// api_neigh_better — strict relevance ordering of two neighbor elements
// {node,edge,hops}. Lexicographic and comparison-ONLY (no arithmetic): El's `+`
// operator is overloaded to string concatenation, so float scoring like
// weight*salience mis-compiles; ordering by `>`/`<` (always numeric on the
// int64 el_val_t, correct for the non-negative fields here) is safe. Keys, in
// order: fewer hops (closer), stronger edge weight, higher node salience, higher
// node importance. Returns true iff `a` ranks strictly ahead of `b`.
fn api_neigh_better(a: String, b: String) -> Bool {
    let na: String = json_get_raw(a, "node")
    let nb: String = json_get_raw(b, "node")
    let ea: String = json_get_raw(a, "edge")
    let eb: String = json_get_raw(b, "edge")
    let ha: Float = api_float_or(a, "hops", 1.0)
    let hb: Float = api_float_or(b, "hops", 1.0)
    if ha < hb { return true }
    if hb < ha { return false }
    let wa: Float = api_float_or(ea, "weight", 0.0)
    let wb: Float = api_float_or(eb, "weight", 0.0)
    if wa > wb { return true }
    if wb > wa { return false }
    let sa: Float = api_float_or(na, "salience", 0.0)
    let sb: Float = api_float_or(nb, "salience", 0.0)
    if sa > sb { return true }
    if sb > sa { return false }
    let ia: Float = api_float_or(na, "importance", 0.0)
    let ib: Float = api_float_or(nb, "importance", 0.0)
    if ia > ib { return true }
    return false
}

// api_neigh_rank — count of elements that outrank element `i` under the
// api_neigh_better ordering, with array index as the final tiebreak. Element i
// belongs to the content tier iff rank < k. O(n) per element (n bounded ~90
// neighbors), so O(n^2) overall — acceptable for a bounded neighborhood.
fn api_neigh_rank(raw: String, n: Int, i: Int) -> Int {
    let el_i: String = json_array_get(raw, i)
    let better: Int = 0
    let j: Int = 0
    while j < n {
        let el_j: String = json_array_get(raw, j)
        let j_better: Bool = api_neigh_better(el_j, el_i)
        let i_better: Bool = api_neigh_better(el_i, el_j)
        let eq: Bool = !j_better && !i_better
        let wins: Bool = j_better || (eq && j < i)
        let better = if wins { better + 1 } else { better }
        let j = j + 1
    }
    return better
}

// api_neigh_full — top-tier neighbor: the node compacted to a bounded content
// snippet, the full edge raw preserved (guard empty -> null), hops, pointer:false.
fn api_neigh_full(node: String, edge: String, el: String, snip: Int) -> String {
    let e: String = if str_eq(edge, "") { "null" } else { edge }
    return "{\"node\":" + api_compact_node(node, snip)
        + ",\"edge\":" + e
        + ",\"hops\":" + api_num_or_zero(el, "hops")
        + ",\"pointer\":false}"
}

// api_neigh_pointer — tail neighbor: a lightweight, addressable POINTER with NO
// content. Just enough identity (id/label/node_type/tier) to dereference on
// demand, plus edge relation+weight and hops. This is what keeps the payload
// bounded on high-fanout nodes.
fn api_neigh_pointer(node: String, edge: String, el: String) -> String {
    let id: String = json_get(node, "id")
    let label: String = json_get(node, "label")
    let ntype: String = json_get(node, "node_type")
    let tier: String = json_get(node, "tier")
    let relation: String = json_get(edge, "relation")
    return "{\"node\":{\"id\":\"" + api_json_escape(id) + "\""
        + ",\"label\":\"" + api_json_escape(label) + "\""
        + ",\"node_type\":\"" + api_json_escape(ntype) + "\""
        + ",\"tier\":\"" + api_json_escape(tier) + "\"}"
        + ",\"edge\":{\"relation\":\"" + api_json_escape(relation) + "\""
        + ",\"weight\":" + api_num_or_zero(edge, "weight") + "}"
        + ",\"hops\":" + api_num_or_zero(el, "hops")
        + ",\"pointer\":true}"
}

// api_compact_neighbors — bounded projection of an engram neighbor array
// [{node,edge,hops},...]. Relevance-ranks neighbors (via api_neigh_rank /
// api_neigh_better): the top `k_content` are emitted WITH a content snippet; every other neighbor is
// emitted as a lightweight POINTER (no content) the caller dereferences on
// demand. Every element is emitted (as full or pointer), so total fan-out COUNT
// stays visible. Mirrors api_compact_activated but adds the ranking + the
// content/pointer split, keeping high-fanout identity nodes (voice,
// writing-imprint) well under the transport socket-close threshold. Returns a
// valid JSON array.
fn api_compact_neighbors(raw: String, k_content: Int, snip: Int) -> String {
    if !api_nonempty(raw) { return "[]" }
    let n: Int = json_array_len(raw)
    let out: String = "["
    let i: Int = 0
    while i < n {
        let el: String = json_array_get(raw, i)
        let node: String = json_get_raw(el, "node")
        let edge: String = json_get_raw(el, "edge")
        let rank: Int = api_neigh_rank(raw, n, i)
        let sep: String = if i == 0 { "" } else { "," }
        let elem: String = if rank < k_content {
            api_neigh_full(node, edge, el, snip)
        } else {
            api_neigh_pointer(node, edge, el)
        }
        let out = out + sep + elem
        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 (2026-07-30 self-review): this handler was the only
    // working-set endpoint that concatenated UNBOUNDED engram queries —
    // a depth-2 spread PLUS the full neighbor dump of the self-identity hub
    // (highest-fanout node in the graph, ~80KB alone; node JSON carries full
    // content + embeddings). On the ~12k-node store the assembled response
    // ran to multiple MB, 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, and drop the self-hub dump entirely (identity loading has its
    // own dedicated tool, inspectGraph; duplicating it here served nothing).
    // self_neighbors key retained as [] for response-shape compatibility.
    let stats: String = engram_stats_json()
    // PAYLOAD BOUND (2026-07-31): compact every list to a digest. The raw
    // activate/scan builtins emit FULL node objects (content up to ~90KB each);
    // unbounded concatenation reached ~900KB and closed the MCP client socket.
    // Cap counts + project to identity + UTF-8-safe content snippets → <~150KB.
    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 (2026-07-31): 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 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))
    }
    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.
// 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")
    // Optional bounded projection. `compact=1` relevance-ranks the neighborhood
    // (top-K get content snippets, the rest become lightweight pointers) so the
    // MCP transport never socket-closes on high-fanout identity anchors (voice,
    // writing-imprint). Absent the flag the studio app's calls are UNCHANGED.
    let compact: String = if str_eq(method, "GET") { api_query_param(path, "compact") } else { json_get(body, "compact") }
    if str_eq(compact, "1") || str_eq(compact, "true") {
        let snip_q: Int = api_query_int(path, "snip", 0)
        let snip: Int = if snip_q == 0 { 600 } else { snip_q }
        let k_q: Int = api_query_int(path, "k", 0)
        let k: Int = if k_q == 0 { 12 } else { k_q }
        return api_or_empty(api_compact_neighbors(results, k, snip))
    }
    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 + "\"}"
}
