1519 lines
81 KiB
EmacsLisp
1519 lines
81 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_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.
|
|
//
|
|
// WIDENED FOR neuron#117. This function is the single gate every MCP write
|
|
// handler passes through before it reports success (10 call sites), which makes
|
|
// it the right place to close the honesty gap rather than editing ten receipts.
|
|
//
|
|
// It used to read back from engram_get_node_json — the SOUL'S OWN in-process
|
|
// graph. In HTTP-engram mode that asserts the wrong thing: the soul is not the
|
|
// persistence owner, so a node present in its RAM and absent from the owner read
|
|
// as "persisted" and then vanished on the next restart. The guard was doing
|
|
// exactly what its comment promised and still certifying writes that did not
|
|
// survive. It now flushes the write-through spool and asks the OWNER.
|
|
//
|
|
// In file mode (no ENGRAM_URL) the soul IS the owner and wt_commit collapses to
|
|
// the original local read-back — unchanged behaviour, which is what keeps this
|
|
// reversible.
|
|
fn api_persisted(id: String) -> Bool {
|
|
if str_eq(id, "") { return false }
|
|
return wt_commit(id)
|
|
}
|
|
|
|
// 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)
|
|
// SELF-SEEDED SLICE (2026-08-09). The design is explicit: "Every compilation
|
|
// query begins at the self-model node and traverses outward... structural
|
|
// reachability from the self-model node is a precondition for any node to
|
|
// appear in compiled context" (will-anderson patents/drafts/engram-claims.md,
|
|
// Self-Seeded Activation; DRAFT, not a filed provisional — cite it as such).
|
|
//
|
|
// Measured 2026-08-09 before this change: compiled context contained 0-1
|
|
// identity records out of 10, because compilation seeds from a hardcoded
|
|
// TEXT STRING, never from the self. Even an explicit "my values identity who
|
|
// I am" query returned a boot counter and state-events.
|
|
//
|
|
// This restores the designed behaviour WITHOUT repeating the failure that got
|
|
// self_neighbors set to [] in the first place: that was an UNBOUNDED ~90KB
|
|
// neighbour dump which closed the socket on every call. Same bound as every
|
|
// other list here — cap 8, 240-char snippets. The self root has 34 direct
|
|
// neighbours of which 23 are identity records, so depth 1 is dense enough to
|
|
// be worth seeding and small enough to stay cheap.
|
|
let self_raw: String = engram_neighbors_json("kn-efeb4a5b-5aff-4759-8a97-7233099be6ee", 1, "both")
|
|
// Cap 24, not 8: measured 2026-08-09, the self root's first 8 neighbours are
|
|
// TAG nodes ("neuron", "tier:note", "disposition:experimental", "imprint",
|
|
// "traversal") which crowd out the substantive identity records behind them.
|
|
// The root has 34 neighbours of which 23 are identity; 24 captures them while
|
|
// staying bounded. Cost measured at ~+4KB on a ~12KB response, nowhere near
|
|
// the ~90KB unbounded dump that closed sockets and got this set to [].
|
|
let self_slice: String = api_compact_node_array(self_raw, 24, 240)
|
|
return "{\"stats\":" + stats
|
|
+ ",\"recent\":" + recent
|
|
+ ",\"activated\":" + activated
|
|
+ ",\"self_neighbors\":" + self_slice
|
|
+ ",\"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 = wt_node(content, "Memory", "memory:remembered",
|
|
sal, sal, el_from_float(0.9),
|
|
"Episodic", final_tags)
|
|
if !api_persisted(id) { return api_not_persisted(id) }
|
|
// Associate on write (2026-08-09). THIS CALL MUST BE HERE, not only in mem_store.
|
|
// The HTTP memory route writes via wt_node directly; mem_store serves only the
|
|
// awareness paths (soul-response, search-result, activation-result) which are
|
|
// exactly the telemetry we refuse to link. Hooking mem_store alone produced
|
|
// ZERO edges across four real writes — measured, not assumed, which is the only
|
|
// reason it was caught before shipping.
|
|
mem_associate(id, content, "memory:remembered")
|
|
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 = wt_node(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 = wt_node(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) }
|
|
wt_edge(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 = wt_node(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 = wt_node(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, "") {
|
|
wt_edge(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 = wt_node(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) }
|
|
wt_edge(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 = wt_node(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 = wt_node(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 }
|
|
wt_edge(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 = wt_node(content, "Memory", "memory:evolved",
|
|
sal, sal, el_from_float(0.9),
|
|
"Episodic", tags)
|
|
if !str_eq(prior_id, "") && !str_eq(new_id, "") {
|
|
wt_edge(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 = wt_node(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, "") {
|
|
wt_edge(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 = wt_node(content, "Memory", "memory:cultivated",
|
|
sal, sal, el_from_float(0.9),
|
|
"Episodic", tags)
|
|
if !str_eq(prior_id, "") && !str_eq(new_id, "") {
|
|
wt_edge(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 }
|
|
wt_edge(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 = wt_node(
|
|
"[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 + "\"}"
|
|
}
|
|
|
|
// ── Stage 1: structural audit ─────────────────────────────────────────────────
|
|
//
|
|
// WHAT THIS IMPLEMENTS
|
|
// The CGI provisional, 05-detailed-description.md, "Stage 1: Structural audit
|
|
// 430". Verbatim, the audit module evaluates: the density and typed
|
|
// distribution of causal edges; the consistency between value nodes and
|
|
// execution-record neighborhoods; the richness and connectivity of the
|
|
// self-model; and the authenticity of open-question nodes in the wonder
|
|
// manifest. It "produces a coherence assessment 432 — NOT A BINARY SCORE but
|
|
// an annotated characterization of the graph's structural properties".
|
|
//
|
|
// That last clause is the whole shape of this handler. Every finding carries
|
|
// its own numbers AND a plain-language note saying what the numbers mean and
|
|
// how they were obtained. There is no pass/fail, no percentage-of-health, no
|
|
// composite score, and `"score":null` is emitted explicitly so a downstream
|
|
// reader cannot mistake its absence for an omission.
|
|
//
|
|
// WHY IT EXISTS NOW, AND WHY THE FIRST FINDING IS THE ONE IT IS
|
|
// `runStructuralAudit` has been an advertised MCP tool with nothing behind it:
|
|
// the dispatcher GET'd /session/begin and returned that blob (mcp-wrapper/src/
|
|
// main.el). Meanwhile the failure the audit would have caught ran silently for
|
|
// about three weeks — the soul reported 103,089 nodes while the engram, which
|
|
// OWNS persistence, held ~79,900; a crash discarded the difference. Every boot
|
|
// reported green throughout, because nothing in the system ever compared the
|
|
// two sides. So finding 1 is owner-versus-runtime divergence: it is the check
|
|
// whose absence cost real memory, and it is cheap and exact.
|
|
//
|
|
// WHAT IS DELIBERATELY NOT HERE (stage 1b, see the `deferred` array in the
|
|
// response): value/execution-record consistency and wonder-manifest
|
|
// authenticity. Both need node types that barely exist in this graph today —
|
|
// the response MEASURES those populations and reports the counts as the reason,
|
|
// rather than asserting a deferral without evidence.
|
|
//
|
|
// MEASUREMENT HONESTY: EXACT WHERE CHEAP, SAMPLED WHERE NOT, ALWAYS LABELLED
|
|
// Counts, edge typing and self-model connectivity are exact. Orphan rate and
|
|
// dangling-edge rate are SAMPLED, because the engram runtime has no node-id
|
|
// index — `engram_find_node_index` is a linear scan over every node, so an
|
|
// exhaustive dangling check is O(nodes x edges) (~2.2e9 string compares at
|
|
// today's scale, tens of seconds inside one request). The samples are UNIFORM
|
|
// across the whole population, not head-of-list, and every sampled figure is
|
|
// emitted with its own `sampled` / `population` fields plus an extrapolation
|
|
// labelled as such. Raise `?edge_sample=` / `?node_sample=` to the population
|
|
// size to run either check exhaustively and pay the time. The real fix is an
|
|
// id index in the runtime; that is the engram repo's, not this handler's.
|
|
|
|
// audit_pct1 — one-decimal percentage as a bare JSON number, sign-safe.
|
|
// Integer math only: EL has no fixed-precision formatter, and float_to_str
|
|
// would put an unbounded mantissa in the response.
|
|
fn audit_pct1(num: Int, den: Int) -> String {
|
|
if den <= 0 { return "null" }
|
|
let neg: Bool = num < 0
|
|
let a: Int = if neg { 0 - num } else { num }
|
|
let tenths: Int = (a * 1000) / den
|
|
let whole: Int = tenths / 10
|
|
let frac: Int = tenths - (whole * 10)
|
|
let sign: String = if neg { "-" } else { "" }
|
|
return sign + int_to_str(whole) + "." + int_to_str(frac)
|
|
}
|
|
|
|
// audit_finding — the one envelope every finding uses: name, the measurements,
|
|
// and the annotation. Keeping it in one place is what stops the characterization
|
|
// from degenerating into a bag of numbers with no reading attached.
|
|
fn audit_finding(name: String, measured: String, note: String) -> String {
|
|
return "{\"finding\":\"" + name + "\""
|
|
+ ",\"measured\":{" + measured + "}"
|
|
+ ",\"note\":\"" + api_json_escape(note) + "\"}"
|
|
}
|
|
|
|
// audit_str_at — read the quoted string value starting at byte `start`.
|
|
// Slices a bounded window rather than the tail of the (multi-MB) edges array, so
|
|
// this is O(window) per call instead of O(remaining input).
|
|
fn audit_str_at(s: String, start: Int, maxlen: Int) -> String {
|
|
let n: Int = str_len(s)
|
|
if start < 0 || start >= n { return "" }
|
|
let end_guess: Int = start + maxlen
|
|
let stop: Int = if end_guess > n { n } else { end_guess }
|
|
let win: String = str_slice(s, start, stop)
|
|
let q: Int = str_index_of(win, "\"")
|
|
if q < 0 { return "" }
|
|
return str_slice(win, 0, q)
|
|
}
|
|
|
|
// audit_rel_count — exact count of edges carrying `rel`, by scanning the emitted
|
|
// edge array for the literal `"relation":"<rel>"`. engram_emit_edge_json writes
|
|
// metadata ESCAPED as a string, so no nested object can contain that literal and
|
|
// the count cannot be inflated by edge payloads.
|
|
fn audit_rel_count(edges: String, rel: String) -> Int {
|
|
return str_count(edges, "\"relation\":\"" + rel + "\"")
|
|
}
|
|
|
|
// audit_owner_stats — ask the persistence OWNER for its own counts.
|
|
// Returns "" when there is no HTTP owner configured or the owner is unreachable;
|
|
// both are reported as findings, never as a failure of the audit.
|
|
fn audit_owner_stats(url: String) -> String {
|
|
if str_eq(url, "") { return "" }
|
|
return http_get(url + "/api/stats")
|
|
}
|
|
|
|
// audit_divergence — FINDING 1. Runtime (this soul's in-process graph) versus
|
|
// the persistence owner's own count. Trend is measured against the previous
|
|
// audit recorded in soul state, so a second call answers "is the gap growing?"
|
|
// rather than just restating it.
|
|
fn audit_divergence() -> String {
|
|
let rt_nodes: Int = engram_node_count()
|
|
let rt_edges: Int = engram_edge_count()
|
|
let url: String = wt_engram_url()
|
|
|
|
if str_eq(url, "") {
|
|
return audit_finding("owner_runtime_divergence",
|
|
"\"runtime_nodes\":" + int_to_str(rt_nodes)
|
|
+ ",\"runtime_edges\":" + int_to_str(rt_edges)
|
|
+ ",\"owner\":\"none\",\"owner_reachable\":false",
|
|
"No HTTP persistence owner is configured, so this soul IS the owner "
|
|
+ "(file mode) and divergence is not defined. This check only has "
|
|
+ "meaning when ENGRAM_URL points at a separate engram that owns the "
|
|
+ "canonical store.")
|
|
}
|
|
|
|
let stats: String = audit_owner_stats(url)
|
|
// REACHABILITY IS PROVED BY THE PAYLOAD, NOT BY A NON-EMPTY REPLY.
|
|
// http_get does not return "" on a connection failure — it returns a JSON
|
|
// error object ({"error":"Failed to connect to ... Couldn't connect to
|
|
// server"}). Testing only for "" made a DEAD owner read as reachable with
|
|
// node_count 0, i.e. the audit would have reported a 100% divergence and
|
|
// named it as data loss. That false positive is worse than no check at all:
|
|
// it is precisely the kind of confident wrong answer this route exists to
|
|
// stop. Require the field the contract promises.
|
|
let owner_nc_raw: String = json_get_raw(stats, "node_count")
|
|
if str_eq(stats, "") || str_eq(owner_nc_raw, "") {
|
|
return audit_finding("owner_runtime_divergence",
|
|
"\"runtime_nodes\":" + int_to_str(rt_nodes)
|
|
+ ",\"runtime_edges\":" + int_to_str(rt_edges)
|
|
+ ",\"owner\":\"" + api_json_escape(url) + "\",\"owner_reachable\":false"
|
|
+ ",\"owner_reply\":\"" + api_json_escape(api_utf8_trunc(stats, 200)) + "\"",
|
|
"The persistence owner at " + url + " did not return a node_count "
|
|
+ "from GET /api/stats. Divergence is UNKNOWN, NOT ZERO — an owner "
|
|
+ "that cannot be read is exactly the condition under which the "
|
|
+ "runtime's own count means least, and reporting 0 for the owner "
|
|
+ "would manufacture a total-loss reading out of a network error. "
|
|
+ "Reported as a finding rather than raised as an error so the rest "
|
|
+ "of the audit still returns; the owner's raw reply is in "
|
|
+ "owner_reply.")
|
|
}
|
|
|
|
let ow_nodes: Int = json_get_int(stats, "node_count")
|
|
let ow_edges: Int = json_get_int(stats, "edge_count")
|
|
let d_nodes: Int = rt_nodes - ow_nodes
|
|
let d_edges: Int = rt_edges - ow_edges
|
|
|
|
// Trend against the previous audit in this soul's state.
|
|
let prev_raw: String = state_get("audit_prev_node_delta")
|
|
let prev: Int = str_to_int(prev_raw)
|
|
let abs_now: Int = if d_nodes < 0 { 0 - d_nodes } else { d_nodes }
|
|
let abs_prev: Int = if prev < 0 { 0 - prev } else { prev }
|
|
let trend: String = if str_eq(prev_raw, "") {
|
|
"no_prior_audit"
|
|
} else {
|
|
if abs_now > abs_prev { "growing" } else {
|
|
if abs_now < abs_prev { "shrinking" } else { "flat" }
|
|
}
|
|
}
|
|
state_set("audit_prev_node_delta", int_to_str(d_nodes))
|
|
state_set("audit_prev_ts", int_to_str(time_now()))
|
|
|
|
let note_head: String = if d_nodes == 0 {
|
|
"Runtime and owner agree on node count."
|
|
} else {
|
|
"Runtime holds " + int_to_str(d_nodes) + " nodes (" + audit_pct1(d_nodes, rt_nodes)
|
|
+ "% of its own graph) that the persistence owner does not report. Nodes "
|
|
+ "that exist only in runtime memory do not survive a restart."
|
|
}
|
|
return audit_finding("owner_runtime_divergence",
|
|
"\"runtime_nodes\":" + int_to_str(rt_nodes)
|
|
+ ",\"runtime_edges\":" + int_to_str(rt_edges)
|
|
+ ",\"owner\":\"" + api_json_escape(url) + "\",\"owner_reachable\":true"
|
|
+ ",\"owner_nodes\":" + int_to_str(ow_nodes)
|
|
+ ",\"owner_edges\":" + int_to_str(ow_edges)
|
|
+ ",\"node_delta\":" + int_to_str(d_nodes)
|
|
+ ",\"edge_delta\":" + int_to_str(d_edges)
|
|
+ ",\"node_delta_pct_of_runtime\":" + audit_pct1(d_nodes, rt_nodes)
|
|
+ ",\"trend_vs_previous_audit\":\"" + trend + "\""
|
|
+ ",\"previous_node_delta\":" + (if str_eq(prev_raw, "") { "null" } else { int_to_str(prev) }),
|
|
note_head + " Trend against the previous audit recorded in this soul's "
|
|
+ "state: " + trend + ". This is the comparison whose absence let a "
|
|
+ "~24,000-node loss run for weeks with every boot reporting green.")
|
|
}
|
|
|
|
// audit_edge_typing — FINDING 2. Density plus the typed distribution the patent
|
|
// asks for, against the claim-10 relation vocabulary. Exact: str_count over the
|
|
// emitted edge array, one linear pass per relation.
|
|
fn audit_edge_typing(edges: String, total_edges: Int, node_total: Int) -> String {
|
|
let c_sup: Int = audit_rel_count(edges, "Supersedes")
|
|
let c_cau: Int = audit_rel_count(edges, "Causes")
|
|
let c_con: Int = audit_rel_count(edges, "Contains")
|
|
let c_ref: Int = audit_rel_count(edges, "References")
|
|
let c_ctr: Int = audit_rel_count(edges, "Contradicts")
|
|
let c_exe: Int = audit_rel_count(edges, "Exemplifies")
|
|
let c_act: Int = audit_rel_count(edges, "Activates")
|
|
let c_tmp: Int = audit_rel_count(edges, "TemporallyPrecedes")
|
|
let typed: Int = c_sup + c_cau + c_con + c_ref + c_ctr + c_exe + c_act + c_tmp
|
|
|
|
// Lowercase near-misses: the same eight concepts written by the ad-hoc write
|
|
// paths (linkEntities defaults to "associates", linkCausal to "causes").
|
|
// Counted separately because "the vocabulary is unused" and "the vocabulary
|
|
// is used in the wrong case" are different defects with different fixes.
|
|
let l_sup: Int = audit_rel_count(edges, "supersedes")
|
|
let l_cau: Int = audit_rel_count(edges, "causes")
|
|
let l_con: Int = audit_rel_count(edges, "contains")
|
|
let l_ref: Int = audit_rel_count(edges, "references")
|
|
let l_ctr: Int = audit_rel_count(edges, "contradicts")
|
|
let l_exe: Int = audit_rel_count(edges, "exemplifies")
|
|
let l_act: Int = audit_rel_count(edges, "activates")
|
|
let l_tmp: Int = audit_rel_count(edges, "temporallyPrecedes")
|
|
let near: Int = l_sup + l_cau + l_con + l_ref + l_ctr + l_exe + l_act + l_tmp
|
|
|
|
let untyped: Int = total_edges - typed
|
|
return audit_finding("typed_edge_distribution",
|
|
"\"total_edges\":" + int_to_str(total_edges)
|
|
+ ",\"total_nodes\":" + int_to_str(node_total)
|
|
// Density per 100 nodes, not per node: EL has no fixed-precision float
|
|
// formatter, and "0.3 edges per node" rounded to an integer is a lie.
|
|
+ ",\"edges_per_100_nodes\":" + audit_pct1(total_edges, node_total)
|
|
+ ",\"claim10_typed\":" + int_to_str(typed)
|
|
+ ",\"claim10_typed_pct\":" + audit_pct1(typed, total_edges)
|
|
+ ",\"outside_claim10_vocabulary\":" + int_to_str(untyped)
|
|
+ ",\"lowercase_near_miss\":" + int_to_str(near)
|
|
+ ",\"by_relation\":{"
|
|
+ "\"Supersedes\":" + int_to_str(c_sup)
|
|
+ ",\"Causes\":" + int_to_str(c_cau)
|
|
+ ",\"Contains\":" + int_to_str(c_con)
|
|
+ ",\"References\":" + int_to_str(c_ref)
|
|
+ ",\"Contradicts\":" + int_to_str(c_ctr)
|
|
+ ",\"Exemplifies\":" + int_to_str(c_exe)
|
|
+ ",\"Activates\":" + int_to_str(c_act)
|
|
+ ",\"TemporallyPrecedes\":" + int_to_str(c_tmp) + "}",
|
|
"Only " + int_to_str(typed) + " of " + int_to_str(total_edges)
|
|
+ " edges use the claim-10 causal vocabulary; the remainder are ad-hoc "
|
|
+ "relation strings, which is why the graph's causal claims cannot yet "
|
|
+ "be checked for internal consistency — an untyped edge asserts "
|
|
+ "association, not causation. " + int_to_str(near) + " edges use a "
|
|
+ "lowercase spelling of a claim-10 relation: those are near-misses the "
|
|
+ "write paths could be corrected to emit, not genuinely foreign types.")
|
|
}
|
|
|
|
// audit_orphans_dangling — FINDING 3. Both figures are SAMPLED; see the header
|
|
// for why exhaustive is O(nodes x edges) on this runtime.
|
|
//
|
|
// An "orphan" here is a node with zero RESOLVABLE edges: engram_neighbors_json
|
|
// drops any edge whose other endpoint does not resolve to a node, so a node
|
|
// whose only edges are dangling reads as an orphan. That is the right reading —
|
|
// such a node is unreachable by traversal — but it is stated rather than hidden.
|
|
fn audit_orphans_dangling(edges: String, total_edges: Int, node_total: Int,
|
|
edge_cap: Int, node_cap: Int) -> String {
|
|
// ── orphan sample: uniform stride over the node store ──
|
|
let n_take: Int = if node_total < node_cap { node_total } else { node_cap }
|
|
let n_stride: Int = if n_take > 0 { node_total / n_take } else { 1 }
|
|
let n_stride = if n_stride < 1 { 1 } else { n_stride }
|
|
let orphans: Int = 0
|
|
let n_checked: Int = 0
|
|
let j: Int = 0
|
|
while j < n_take {
|
|
let one: String = engram_scan_nodes_json(1, j * n_stride)
|
|
let nid: String = json_get(json_array_get(one, 0), "id")
|
|
if !str_eq(nid, "") {
|
|
let nbrs: String = engram_neighbors_json(nid, 1, "both")
|
|
let deg: Int = json_array_len(nbrs)
|
|
let orphans = if deg == 0 { orphans + 1 } else { orphans }
|
|
let n_checked = n_checked + 1
|
|
}
|
|
let j = j + 1
|
|
}
|
|
|
|
// ── dangling sample: uniform stride over the edge array ──
|
|
// str_index_of_all gives every edge's field offsets in ONE linear pass, so
|
|
// any index can be read in O(1). json_array_get would have been O(i) per
|
|
// element and O(n^2) over the array.
|
|
let from_pos: [Int] = str_index_of_all(edges, "\"from_id\":\"")
|
|
let to_pos: [Int] = str_index_of_all(edges, "\"to_id\":\"")
|
|
let nf: Int = len(from_pos)
|
|
let nt: Int = len(to_pos)
|
|
let ne: Int = if nf < nt { nf } else { nt }
|
|
let e_take: Int = if ne < edge_cap { ne } else { edge_cap }
|
|
let e_stride: Int = if e_take > 0 { ne / e_take } else { 1 }
|
|
let e_stride = if e_stride < 1 { 1 } else { e_stride }
|
|
let dangling: Int = 0
|
|
let e_checked: Int = 0
|
|
let i: Int = 0
|
|
while i < ne && e_checked < e_take {
|
|
let fid: String = audit_str_at(edges, get(from_pos, i) + 11, 96)
|
|
let tid: String = audit_str_at(edges, get(to_pos, i) + 9, 96)
|
|
let f_gone: Bool = str_eq(engram_get_node_json(fid), "{}")
|
|
let t_gone: Bool = if f_gone { true } else { str_eq(engram_get_node_json(tid), "{}") }
|
|
let dangling = if f_gone || t_gone { dangling + 1 } else { dangling }
|
|
let e_checked = e_checked + 1
|
|
let i = i + e_stride
|
|
}
|
|
|
|
let orphan_est: Int = if n_checked > 0 { (orphans * node_total) / n_checked } else { 0 }
|
|
let dangle_est: Int = if e_checked > 0 { (dangling * total_edges) / e_checked } else { 0 }
|
|
let exhaustive_n: String = if n_checked >= node_total { "true" } else { "false" }
|
|
let exhaustive_e: String = if e_checked >= ne { "true" } else { "false" }
|
|
|
|
return audit_finding("orphans_and_dangling_edges",
|
|
"\"nodes_population\":" + int_to_str(node_total)
|
|
+ ",\"nodes_sampled\":" + int_to_str(n_checked)
|
|
+ ",\"nodes_sample_exhaustive\":" + exhaustive_n
|
|
+ ",\"orphans_in_sample\":" + int_to_str(orphans)
|
|
+ ",\"orphan_rate_pct\":" + audit_pct1(orphans, n_checked)
|
|
+ ",\"orphans_extrapolated\":" + int_to_str(orphan_est)
|
|
+ ",\"edges_population\":" + int_to_str(total_edges)
|
|
+ ",\"edges_sampled\":" + int_to_str(e_checked)
|
|
+ ",\"edges_sample_exhaustive\":" + exhaustive_e
|
|
+ ",\"dangling_in_sample\":" + int_to_str(dangling)
|
|
+ ",\"dangling_rate_pct\":" + audit_pct1(dangling, e_checked)
|
|
+ ",\"dangling_extrapolated\":" + int_to_str(dangle_est),
|
|
"Orphan = zero RESOLVABLE edges, so a node whose only edges dangle counts "
|
|
+ "as an orphan; either way it is unreachable by traversal. Dangling = an "
|
|
+ "edge with an endpoint id that resolves to no node. Both are uniform "
|
|
+ "stride samples over the whole population, not the head of the list; "
|
|
+ "the extrapolations are estimates and are labelled as such. Pass "
|
|
+ "?node_sample= / ?edge_sample= at or above the population size to run "
|
|
+ "either check exhaustively. A high orphan rate is a characterization, "
|
|
+ "not a verdict: an accumulating store legitimately holds unlinked "
|
|
+ "material. It becomes a defect when the write paths were SUPPOSED to "
|
|
+ "link and did not.")
|
|
}
|
|
|
|
// audit_pillar — one self-model pillar: present, how much content, how connected.
|
|
fn audit_pillar(key: String, id: String) -> String {
|
|
let node: String = engram_get_node_json(id)
|
|
let present: Bool = !str_eq(node, "{}") && !str_eq(node, "")
|
|
if !present {
|
|
return "\"" + key + "\":{\"id\":\"" + id + "\",\"present\":false"
|
|
+ ",\"content_length\":0,\"degree\":0}"
|
|
}
|
|
let content: String = json_get(node, "content")
|
|
let deg: Int = json_array_len(engram_neighbors_json(id, 1, "both"))
|
|
return "\"" + key + "\":{\"id\":\"" + id + "\",\"present\":true"
|
|
+ ",\"label\":\"" + api_json_escape(json_get(node, "label")) + "\""
|
|
+ ",\"tier\":\"" + api_json_escape(json_get(node, "tier")) + "\""
|
|
+ ",\"content_length\":" + int_to_str(str_len(content))
|
|
+ ",\"degree\":" + int_to_str(deg) + "}"
|
|
}
|
|
|
|
// audit_self_model — FINDING 4. "the richness and connectivity of the
|
|
// self-model ... is it connected to behavioral evidence?"
|
|
//
|
|
// This finding RETIRES the Claude-side vitals identity block. That check lived
|
|
// outside the system it was checking — a shell script grepping a snapshot — so
|
|
// it could only ever report on a file, and it went on reporting green while the
|
|
// memory-philosophy pillar was absent from the live graph for about three weeks.
|
|
// Asking the running soul about its own three pillars is the designed mechanism;
|
|
// a shell probe was the fourth patch on the same hole.
|
|
fn audit_self_model() -> String {
|
|
let dna: String = audit_pillar("intellectual_dna", "kn-5adecd7e-d6db-4576-87fe-6ef8a935cea6")
|
|
let val: String = audit_pillar("values_hub", "kn-5b606390-a52d-4ca2-8e0e-eba141d13440")
|
|
let phi: String = audit_pillar("memory_philosophy", "kn-dcfe04b3-3702-4cac-b6f0-ecb4db837eee")
|
|
let root: String = audit_pillar("self_root", "kn-efeb4a5b-5aff-4759-8a97-7233099be6ee")
|
|
return audit_finding("self_model_connectivity",
|
|
"\"pillars\":{" + dna + "," + val + "," + phi + "," + root + "}",
|
|
"The three identity pillars plus the self root. `degree` counts nodes "
|
|
+ "reachable in one hop in either direction — the self-model's connection "
|
|
+ "to the rest of the graph. present:false on any pillar is the condition "
|
|
+ "that ran undetected for weeks; content_length distinguishes a pillar "
|
|
+ "that is present from one that is present but hollowed out. The patent "
|
|
+ "also asks whether the self-model makes ACCURATE PREDICTIONS about the "
|
|
+ "system's own behavior; that half needs Prediction nodes and is deferred "
|
|
+ "with the rest of stage 1b below.")
|
|
}
|
|
|
|
// audit_deferred — what stage 1 does NOT yet evaluate, with the measured reason.
|
|
// Emitted as data, not as a comment, so a reader of the assessment sees the gap
|
|
// and its evidence rather than inferring completeness from silence.
|
|
fn audit_deferred() -> String {
|
|
let preds: Int = json_array_len(api_or_empty(engram_scan_nodes_by_type_json("Prediction", 50, 0)))
|
|
let wonders: Int = json_array_len(api_or_empty(engram_scan_nodes_by_type_json("WonderQuestion", 50, 0)))
|
|
return "[{\"deferred\":\"value_execution_record_consistency\""
|
|
+ ",\"stage\":\"1b\""
|
|
+ ",\"measured\":{\"prediction_nodes_found\":" + int_to_str(preds) + "}"
|
|
+ ",\"reason\":\"" + api_json_escape(
|
|
"The patent asks whether the execution history SUPPORTS the stated "
|
|
+ "values or shows systematic conflict. That requires execution "
|
|
+ "records tied to value nodes and predictions to score them against. "
|
|
+ "Prediction nodes found (capped at 50): " + int_to_str(preds)
|
|
+ ". Asserting value/execution coherence on that population would be "
|
|
+ "a fabricated result, which is worse than a stated gap.") + "\"}"
|
|
+ ",{\"deferred\":\"wonder_manifest_authenticity\""
|
|
+ ",\"stage\":\"1b\""
|
|
+ ",\"measured\":{\"wonder_question_nodes_found\":" + int_to_str(wonders) + "}"
|
|
+ ",\"reason\":\"" + api_json_escape(
|
|
"The patent asks whether pull weights CORRELATE WITH GENUINE "
|
|
+ "PREDICTION UNCERTAINTY or are uniform/externally assigned — a "
|
|
+ "correlation between two populations. WonderQuestion nodes readable "
|
|
+ "by type (capped at 50): " + int_to_str(wonders) + ", against "
|
|
+ int_to_str(preds) + " Prediction nodes. There is a known write/read "
|
|
+ "node-type mismatch on the wonder path; until that is fixed and both "
|
|
+ "populations exist, any correlation reported here would be noise.") + "\"}]"
|
|
}
|
|
|
|
// handle_api_structural_audit — Stage 1. Returns the coherence assessment 432:
|
|
// an annotated characterization, explicitly NOT a score.
|
|
//
|
|
// COST NOTE: the edge findings need the relation labels, and the runtime exposes
|
|
// no edge-enumeration builtin. The only way to see them is the same one
|
|
// GET /api/graph/edges already uses — engram_save to a SCRATCH path (never the
|
|
// owner's canonical file; see routes.el, neuron#117) and read the array back.
|
|
// On a large graph that is a multi-hundred-MB write, so this is a manual audit
|
|
// route, not something to put on a timer. Pass ?edges=0 to skip both edge
|
|
// findings and get the divergence + self-model readings cheaply.
|
|
fn handle_api_structural_audit(method: String, path: String, body: String) -> String {
|
|
let node_total: Int = engram_node_count()
|
|
let edge_total: Int = engram_edge_count()
|
|
let want_edges: Bool = !str_eq(api_query_param(path, "edges"), "0")
|
|
let edge_cap: Int = api_query_int(path, "edge_sample", 3000)
|
|
let node_cap: Int = api_query_int(path, "node_sample", 300)
|
|
|
|
let divergence: String = audit_divergence()
|
|
let self_model: String = audit_self_model()
|
|
|
|
let edge_part: String = if want_edges {
|
|
// Scratch export only. state_get("soul_snapshot_path") is deliberately
|
|
// NOT used: in HTTP-engram mode the soul is not the persistence owner and
|
|
// must never write the canonical file, not even on a read path.
|
|
let scratch_dir: String = env("TMPDIR")
|
|
let scratch_base: String = if str_eq(scratch_dir, "") { "/tmp" } else { scratch_dir }
|
|
let snap_path: String = scratch_base + "/soul-audit-export-" + state_get("soul_cgi_id") + ".json"
|
|
// engram_save returns Int (1 ok / 0 fail); str_eq on it SIGSEGVs (#150).
|
|
let saved: Int = engram_save(snap_path)
|
|
if saved == 0 {
|
|
"," + audit_finding("typed_edge_distribution", "\"available\":false",
|
|
"Could not export the graph to " + snap_path + " for edge analysis, "
|
|
+ "so edge typing and the dangling-edge sample were not run. "
|
|
+ "Reported as a gap, not as zero findings.")
|
|
} else {
|
|
// wt_read, not fs_read: fs_read leaves a thread-local length hint that
|
|
// the NEXT HTTP response would use as its Content-Length, appending
|
|
// adjacent heap bytes to the reply (see persist.el wt_read).
|
|
let snap: String = wt_read(snap_path)
|
|
let edges_raw: String = json_get_raw(snap, "edges")
|
|
let edges: String = if str_eq(edges_raw, "") { "[]" } else { edges_raw }
|
|
"," + audit_edge_typing(edges, edge_total, node_total)
|
|
+ "," + audit_orphans_dangling(edges, edge_total, node_total, edge_cap, node_cap)
|
|
}
|
|
} else {
|
|
""
|
|
}
|
|
|
|
return "{\"audit\":\"structural\",\"stage\":1"
|
|
+ ",\"spec\":\"CGI provisional 05-detailed-description.md, Stage 1: Structural audit 430\""
|
|
+ ",\"assessment\":\"coherence_assessment_432\""
|
|
+ ",\"assessment_kind\":\"annotated_characterization\""
|
|
+ ",\"score\":null"
|
|
+ ",\"score_note\":\"By design. The specification calls for an annotated characterization of the graph's structural properties, not a binary score. Read the findings.\""
|
|
+ ",\"cgi_id\":\"" + api_json_escape(state_get("soul_cgi_id")) + "\""
|
|
+ ",\"ts_ms\":" + int_to_str(time_now())
|
|
+ ",\"findings\":[" + divergence + "," + self_model + edge_part + "]"
|
|
+ ",\"deferred\":" + audit_deferred() + "}"
|
|
}
|