d3d550664e
El SDK CI - dev / build-and-test (pull_request) Failing after 14m53s
Collapses three separate mutation paths (node write, node supersede, and ad-hoc bulk edits) into a single reframe_core: isolate a region (cosine + 1-hop adjacency), tombstone-supersede it as an immutable set, insert the replacement manifold, rebind edges by cosine, and persist atomically. write and supersede become the region-size-0 and region-size-1 degenerate cases of the same path, eliminating divergent per-route logic. Identity keystones remain write-protected across all three entry points.
831 lines
40 KiB
EmacsLisp
831 lines
40 KiB
EmacsLisp
// server.el — Engram HTTP server.
|
||
//
|
||
// Engram is the in-process graph store. The runtime owns the data; this
|
||
// file is the thin HTTP face. Every route maps to one or two engram_*
|
||
// builtins. There is no SQL, no db layer, no SQLite — the runtime IS the
|
||
// database.
|
||
//
|
||
// Built and linked with:
|
||
// elc src/server.el > server.c
|
||
// cc -std=c11 -O2 -lcurl -lpthread -o engram server.c el_runtime.c
|
||
// ./engram
|
||
//
|
||
// Configuration via environment:
|
||
// ENGRAM_BIND — host:port (default :8742)
|
||
// ENGRAM_API_KEY — bearer auth (optional)
|
||
// ENGRAM_DATA_DIR — snapshot location (default ~/.neuron/engram)
|
||
|
||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||
|
||
fn parse_port(bind: String) -> Int {
|
||
// ":8742" → 8742; "0.0.0.0:8742" → 8742; bare "8742" → 8742
|
||
let colon: Int = str_index_of(bind, ":")
|
||
if colon < 0 {
|
||
return str_to_int(bind)
|
||
}
|
||
let after: String = str_slice(bind, colon + 1, str_len(bind))
|
||
return str_to_int(after)
|
||
}
|
||
|
||
fn ok_json() -> String {
|
||
"{\"ok\":true}"
|
||
}
|
||
|
||
fn err_json(msg: String) -> String {
|
||
"{\"error\":\"" + msg + "\"}"
|
||
}
|
||
|
||
fn strip_query(path: String) -> String {
|
||
let q: Int = str_index_of(path, "?")
|
||
if q < 0 { return path }
|
||
str_slice(path, 0, q)
|
||
}
|
||
|
||
fn 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 }
|
||
str_slice(after, 0, amp)
|
||
}
|
||
|
||
fn query_int(path: String, key: String, default_val: Int) -> Int {
|
||
let v: String = query_param(path, key)
|
||
if str_eq(v, "") { return default_val }
|
||
str_to_int(v)
|
||
}
|
||
|
||
// Extract last path segment after a known prefix: extract_id("/api/nodes/abc-123", "/api/nodes/") → "abc-123"
|
||
fn extract_id(path: String, prefix: String) -> String {
|
||
let clean: String = strip_query(path)
|
||
if !str_starts_with(clean, prefix) { return "" }
|
||
let after: String = str_slice(clean, str_len(prefix), str_len(clean))
|
||
let slash: Int = str_index_of(after, "/")
|
||
if slash < 0 { return after }
|
||
str_slice(after, 0, slash)
|
||
}
|
||
|
||
// ── Routes ────────────────────────────────────────────────────────────────────
|
||
|
||
fn route_stats(method: String, path: String, body: String) -> String {
|
||
engram_stats_json()
|
||
}
|
||
|
||
// (2026-07-18 self-review) Scoping sweep: `let` inside an if-block creates an
|
||
// inner scope only — it does NOT mutate the outer binding (documented with
|
||
// evidence in awareness.el, 2026-05-25). Every default/reassignment below used
|
||
// that broken pattern, so defaults never applied: nodes were created with
|
||
// node_type="" and salience=0.0, /api/search and /api/activate ALWAYS ran with
|
||
// q="" regardless of input, edges defaulted to relation=""/weight=0.0, and
|
||
// save/load with no "path" hit engram_save(""). Rewritten to the
|
||
// `let x = if cond { a } else { b }` expression form (the pattern the newer
|
||
// routes route_emit_ise/route_capture_knowledge already use correctly).
|
||
// persist_canonical — save the canonical snapshot after a durable write.
|
||
//
|
||
// WHY (2026-07-22 self-review): the 2026-07-21 fix correctly stopped READ
|
||
// routes from writing the canonical snapshot.json — but nothing was left
|
||
// that saved it on WRITE. Every mutation (node create, edge create,
|
||
// knowledge capture, forget, merge) lived only in RAM until someone POSTed
|
||
// /api/save manually; a process restart silently discarded everything since
|
||
// the last manual save. Observed live: two engram restarts during the
|
||
// 2026-07-22 review reverted the store to a ~17h-old snapshot, destroying
|
||
// same-day writes. Reads must never write the canonical; writes must always
|
||
// persist it. ISE telemetry is deliberately excluded (48h-pruned, loss-
|
||
// tolerant, ~2/min — snapshotting the whole store per heartbeat is waste;
|
||
// any durable write that follows persists the pruning too).
|
||
fn persist_canonical() -> Int {
|
||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||
engram_save(dir + "/snapshot.json")
|
||
return 1
|
||
}
|
||
|
||
fn route_create_node(method: String, path: String, body: String) -> String {
|
||
let content: String = json_get_string(body, "content")
|
||
let nt_raw: String = json_get_string(body, "node_type")
|
||
let node_type: String = if str_eq(nt_raw, "") { "Memory" } else { nt_raw }
|
||
let sal_raw: Float = json_get_float(body, "salience")
|
||
let salience: Float = if sal_raw == 0.0 { 0.5 } else { sal_raw }
|
||
let id: String = engram_node(content, node_type, salience)
|
||
let saved: Int = persist_canonical()
|
||
"{\"id\":\"" + id + "\",\"content\":\"" + content + "\",\"node_type\":\"" + node_type + "\"}"
|
||
}
|
||
|
||
fn route_get_node(method: String, path: String, body: String) -> String {
|
||
let id: String = extract_id(path, "/api/nodes/")
|
||
if str_eq(id, "") { return err_json("missing id") }
|
||
return engram_get_node_json(id)
|
||
}
|
||
|
||
fn route_scan_nodes(method: String, path: String, body: String) -> String {
|
||
let limit: Int = query_int(path, "limit", 50)
|
||
let offset: Int = query_int(path, "offset", 0)
|
||
let nt: String = query_param(path, "node_type")
|
||
if str_eq(nt, "") {
|
||
return engram_scan_nodes_json(limit, offset)
|
||
}
|
||
return engram_scan_nodes_by_type_json(nt, limit, offset)
|
||
}
|
||
|
||
// route_scan_edges — bulk export of all edges as a JSON array. Implemented
|
||
// via engram_save → fs_read of a SCRATCH export path. (2026-07-21 self-review:
|
||
// previously this saved over the canonical snapshot.json on every GET — if the
|
||
// process ever booted with a partial/empty store, the first read request
|
||
// clobbered the good snapshot. Read routes must never write the canonical path.)
|
||
fn route_scan_edges(method: String, path: String, body: String) -> String {
|
||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||
let snap_path: String = dir + "/.scan-export.json"
|
||
engram_save(snap_path)
|
||
let snap: String = fs_read(snap_path)
|
||
if str_eq(snap, "") { return "[]" }
|
||
// json_get truncates at the first delimiter (no bracket depth tracking),
|
||
// so for the edges ARRAY value we need json_get_raw, which honors
|
||
// brackets and returns the full sub-JSON.
|
||
let edges: String = json_get_raw(snap, "edges")
|
||
if str_eq(edges, "") { return "[]" }
|
||
return edges
|
||
}
|
||
|
||
fn route_search(method: String, path: String, body: String) -> String {
|
||
let q: String = if str_eq(method, "GET") { query_param(path, "q") } else { json_get_string(body, "query") }
|
||
let lim_url: Int = query_int(path, "limit", 0)
|
||
let lim_body: Int = json_get_int(body, "limit")
|
||
let lim_either: Int = if lim_url > 0 { lim_url } else { lim_body }
|
||
let limit: Int = if lim_either > 0 { lim_either } else { 20 }
|
||
return engram_search_json(q, limit)
|
||
}
|
||
|
||
fn route_activate(method: String, path: String, body: String) -> String {
|
||
let q: String = if str_eq(method, "GET") { query_param(path, "q") } else { json_get_string(body, "query") }
|
||
// Guard: engram_activate with an empty query matches zero seeds, which
|
||
// zeroes ALL carried working-memory weights (documented in awareness.el
|
||
// perceive()). Never let an empty activation through to wipe WM.
|
||
if str_eq(q, "") { return err_json("missing query") }
|
||
let d_raw: Int = if str_eq(method, "GET") { query_int(path, "depth", 3) } else { json_get_int(body, "depth") }
|
||
let depth: Int = if d_raw > 0 { d_raw } else { 3 }
|
||
return "{\"results\":" + engram_activate_json(q, depth) + "}"
|
||
}
|
||
|
||
fn route_create_edge(method: String, path: String, body: String) -> String {
|
||
let from_id: String = json_get_string(body, "from_id")
|
||
let to_id: String = json_get_string(body, "to_id")
|
||
let rel_raw: String = json_get_string(body, "relation")
|
||
let relation: String = if str_eq(rel_raw, "") { "associates" } else { rel_raw }
|
||
let w_raw: Float = json_get_float(body, "weight")
|
||
let weight: Float = if w_raw == 0.0 { 0.5 } else { w_raw }
|
||
engram_connect(from_id, to_id, weight, relation)
|
||
let saved: Int = persist_canonical()
|
||
"{\"ok\":true,\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + relation + "\"}"
|
||
}
|
||
|
||
fn route_neighbors(method: String, path: String, body: String) -> String {
|
||
let id: String = extract_id(path, "/api/neighbors/")
|
||
if str_eq(id, "") { return err_json("missing id") }
|
||
let depth: Int = query_int(path, "depth", 1)
|
||
return engram_neighbors_json(id, depth, "both")
|
||
}
|
||
|
||
fn route_strengthen(method: String, path: String, body: String) -> String {
|
||
let id: String = json_get_string(body, "node_id")
|
||
if str_eq(id, "") { return err_json("missing node_id") }
|
||
engram_strengthen(id)
|
||
let saved: Int = persist_canonical()
|
||
ok_json()
|
||
}
|
||
|
||
fn route_forget(method: String, path: String, body: String) -> String {
|
||
let id: String = extract_id(path, "/api/nodes/")
|
||
if str_eq(id, "") { return err_json("missing id") }
|
||
engram_forget(id)
|
||
let saved: Int = persist_canonical()
|
||
ok_json()
|
||
}
|
||
|
||
fn route_save(method: String, path: String, body: String) -> String {
|
||
let p_raw: String = json_get_string(body, "path")
|
||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||
let p: String = if str_eq(p_raw, "") { dir + "/snapshot.json" } else { p_raw }
|
||
engram_save(p)
|
||
"{\"ok\":true,\"path\":\"" + p + "\"}"
|
||
}
|
||
|
||
fn route_load(method: String, path: String, body: String) -> String {
|
||
let p_raw: String = json_get_string(body, "path")
|
||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||
let p: String = if str_eq(p_raw, "") { dir + "/snapshot.json" } else { p_raw }
|
||
engram_load(p)
|
||
ok_json()
|
||
}
|
||
|
||
fn route_health(method: String, path: String, body: String) -> String {
|
||
"{\"status\":\"ok\",\"engine\":\"engram-runtime-native\"}"
|
||
}
|
||
|
||
// route_sync — return a snapshot of non-ISE/non-Working nodes for the soul daemon
|
||
// to merge into its in-process graph via engram_load_merge.
|
||
//
|
||
// The soul calls GET /api/sync every SOUL_REFRESH_MS (default 10 min) to pull
|
||
// new Knowledge/Memory/BacklogItem nodes from the authoritative HTTP Engram into
|
||
// its in-process working store. Previously this returned 404 "not found", causing
|
||
// the soul to write the error JSON to a temp file and attempt an empty merge.
|
||
//
|
||
// Strategy: save the current snapshot to disk, read it back, return the full
|
||
// snapshot JSON. The soul's engram_load_merge handles large files gracefully
|
||
// (it skips nodes already present by ID). Auth-exempt: same-host internal call.
|
||
// (2026-06-27 self-review: added this route to fix silent 10-min sync failures)
|
||
fn route_sync(method: String, path: String, body: String) -> String {
|
||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||
// 2026-07-21 self-review: export to a scratch path, never the canonical
|
||
// snapshot.json — read routes must not be able to clobber the good snapshot.
|
||
let snap_path: String = dir + "/.sync-export.json"
|
||
engram_save(snap_path)
|
||
let snap: String = fs_read(snap_path)
|
||
if str_eq(snap, "") { return "{\"nodes\":[],\"edges\":[]}" }
|
||
return snap
|
||
}
|
||
|
||
// route_load_merge — POST /api/load-merge {"path": "..."} — merge a snapshot
|
||
// file into the live store WITHOUT resetting it (engram_load_merge skips nodes
|
||
// already present by id). Added 2026-07-21 self-review to restore the 244 kn-
|
||
// identity Knowledge nodes lost from the snapshot lineage between 05-13 and
|
||
// 07-13. Requires an explicit path: refuses to run without one so it can never
|
||
// be triggered accidentally against a default.
|
||
fn route_load_merge(method: String, path: String, body: String) -> String {
|
||
let p: String = json_get_string(body, "path")
|
||
if str_eq(p, "") { return err_json("path is required") }
|
||
if str_eq(fs_read(p), "") { return err_json("file missing or empty") }
|
||
let before_n: Int = engram_node_count()
|
||
let before_e: Int = engram_edge_count()
|
||
engram_load_merge(p)
|
||
let added_n: Int = engram_node_count() - before_n
|
||
let added_e: Int = engram_edge_count() - before_e
|
||
let saved: Int = persist_canonical()
|
||
"{\"ok\":true,\"nodes_added\":" + int_to_str(added_n) + ",\"edges_added\":" + int_to_str(added_e) + ",\"node_count\":" + int_to_str(engram_node_count()) + "}"
|
||
}
|
||
|
||
// route_emit_ise — write an InternalStateEvent node from the soul daemon.
|
||
//
|
||
// Endpoint: POST /api/neuron/state-events
|
||
// Body: {"content": "<json-string>"}
|
||
//
|
||
// Auth: exempt (internal endpoint, soul daemon on same host, no _auth needed).
|
||
// The soul's ise_post() sends {"content":"..."} without _auth; enforcing auth
|
||
// here would silently drop all heartbeat/curiosity ISEs. Unauthenticated POST
|
||
// to this endpoint is acceptable: ISE writes are observability-only, append-only,
|
||
// and come from a trusted process on localhost.
|
||
//
|
||
// Salience/importance set to match engram_node_full ISE defaults used by the
|
||
// in-process fallback path in awareness.el (salience=0.3, importance=0.3,
|
||
// confidence=0.8, tier=Episodic).
|
||
// (2026-06-26 self-review: added this route after discovering ise_post was
|
||
// silently failing — the soul posts here but the endpoint didn't exist.)
|
||
//
|
||
// Retention (2026-07-16 self-review): an earlier comment here claimed ISEs
|
||
// got temporal_decay_rate=1.617 — that was never implemented (engram_node_full
|
||
// hardcodes 0.0), and per-node decay only dampens activation anyway; it never
|
||
// removes nodes. By 2026-07-16 ISEs were 75% of the store (10,175 of 13,522
|
||
// nodes, ~4,300/day, unbounded). ISEs are already WM-excluded in
|
||
// engram_activate, so the fix is retention, not decay: every insert calls
|
||
// engram_prune_telemetry(), a single O(nodes+edges) compaction pass that
|
||
// removes ISEs older than ENGRAM_ISE_RETENTION_MS (default 48h), protecting
|
||
// "session-start" labels and self_review events as durable history. At
|
||
// ~3 ISEs/min this bounds telemetry at ~8.6k nodes instead of growing forever.
|
||
fn route_emit_ise(method: String, path: String, body: String) -> String {
|
||
let content: String = json_get_string(body, "content")
|
||
if str_eq(content, "") { return err_json("missing content") }
|
||
let sal: Float = 0.3
|
||
let imp: Float = 0.3
|
||
let conf: Float = 0.8
|
||
let id: String = engram_node_full(
|
||
content, "InternalStateEvent", "state-event",
|
||
sal, imp, conf,
|
||
"Episodic", "[\"internal-state\",\"InternalStateEvent\"]"
|
||
)
|
||
let ret_raw: String = env("ENGRAM_ISE_RETENTION_MS")
|
||
let ret_ms: Int = if str_eq(ret_raw, "") { 172800000 } else { str_to_int(ret_raw) }
|
||
let pruned: Int = engram_prune_telemetry(ret_ms)
|
||
"{\"ok\":true,\"id\":\"" + id + "\",\"pruned\":" + int_to_str(pruned) + "}"
|
||
}
|
||
|
||
// ── Knowledge capture ─────────────────────────────────────────────────────────
|
||
//
|
||
// route_capture_knowledge — direct Knowledge-node capture over HTTP.
|
||
//
|
||
// Endpoint: POST /api/neuron/knowledge/capture (auth required: "_auth" in body)
|
||
// Body: {"content": "...", "title": "...", "category": "...",
|
||
// "tier": "note|lesson|canonical", "tags": [...], "project": "...",
|
||
// "_auth": "<key>"}
|
||
//
|
||
// WHY (2026-07-15 self-review): the world-ingestor integrator was designed
|
||
// against this endpoint (its MCP-unavailable fallback), but the route never
|
||
// existed — every direct push 404'd, and because the auth gate ran before
|
||
// routing, the failure surfaced as {"error":"unauthorized"} and was
|
||
// misdiagnosed for two weeks while world knowledge silently dropped.
|
||
// POST /api/nodes was no substitute: it discards label/tags/tier, which
|
||
// makes captured knowledge invisible to tag-scoped search and curiosity.
|
||
//
|
||
// The incoming knowledge tier (note/lesson/canonical) is preserved as a
|
||
// "tier:<x>" tag rather than mapped onto Engram's cognitive tiers — Knowledge
|
||
// nodes land in Semantic (stable reference), and the epistemic tier stays
|
||
// queryable without inventing a lossy mapping.
|
||
fn route_capture_knowledge(method: String, path: String, body: String) -> String {
|
||
let content: String = json_get_string(body, "content")
|
||
if str_eq(content, "") { return err_json("missing content") }
|
||
let title: String = json_get_string(body, "title")
|
||
let label: String = if str_eq(title, "") { str_slice(content, 0, 60) } else { title }
|
||
let category_raw: String = json_get_string(body, "category")
|
||
let category: String = if str_eq(category_raw, "") { "other" } else { category_raw }
|
||
let ktier_raw: String = json_get_string(body, "tier")
|
||
let ktier: String = if str_eq(ktier_raw, "") { "note" } else { ktier_raw }
|
||
let project: String = json_get_string(body, "project")
|
||
let tags_raw: String = json_get_raw(body, "tags")
|
||
let tags_base: String = if str_eq(tags_raw, "") { "[]" } else { tags_raw }
|
||
// Merge category/tier/project markers into the tag array. Search matches
|
||
// against the tags string, so these make captures findable by facet.
|
||
let base_len: Int = str_len(tags_base)
|
||
let head: String = str_slice(tags_base, 0, base_len - 1)
|
||
let sep: String = if str_eq(head, "[") { "" } else { "," }
|
||
let safe_cat: String = str_replace(category, "\"", "'")
|
||
let safe_tier: String = str_replace(ktier, "\"", "'")
|
||
let safe_proj: String = str_replace(project, "\"", "'")
|
||
let proj_tag: String = if str_eq(safe_proj, "") { "" } else { ",\"project:" + safe_proj + "\"" }
|
||
let tags: String = head + sep + "\"category:" + safe_cat + "\",\"tier:" + safe_tier + "\"" + proj_tag + "]"
|
||
let sal: Float = 0.5
|
||
let imp: Float = 0.5
|
||
let conf: Float = 0.9
|
||
let id: String = engram_node_full(
|
||
content, "Knowledge", label,
|
||
sal, imp, conf,
|
||
"Semantic", tags
|
||
)
|
||
let saved: Int = persist_canonical()
|
||
"{\"ok\":true,\"id\":\"" + id + "\"}"
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// THE UNIVERSAL ENGRAM OPERATION — reframe_region (native, set-based).
|
||
//
|
||
// There is ONE operation on the engram: isolate a discrete sub-manifold (a
|
||
// REGION) and operate on it AS A WHOLE — a set operation:
|
||
// isolate (cosine retrieval + adjacency → the SET of nodes)
|
||
// → supersede the stale region as a set (immutable tombstone; originals kept)
|
||
// → insert the new manifold as a set (dedup/load-merge path)
|
||
// → rebind edges by cosine
|
||
// → verify + one atomic persist.
|
||
// new = (region superseded) ∪ new_manifold.
|
||
//
|
||
// The SINGLE NODE is the DEGENERATE n=1 case of this SAME operation — not a
|
||
// separate CRUD path:
|
||
// write(content) = reframe(region=∅, manifold=[1 node]) (route_write)
|
||
// supersede(id,new) = reframe(region={id}, manifold=[1 node]) (route_supersede)
|
||
// relate(a,b,rel) = the rebind sub-op in isolation (route_create_edge)
|
||
// The ONLY anti-pattern is decomposing a region-scale change into a LOOP of
|
||
// independent top-level per-node updates. Here the region is the unit: one
|
||
// isolate, one atomic set-replace, one persist, one verify — iterating members
|
||
// INSIDE the one operation is set construction, not the sin.
|
||
//
|
||
// Spec: knowledge e7a03a94 / f999c5ff. Keystones kn-efeb4a5b / kn-5b606390 are
|
||
// write-protected — never superseded, never inserted-as identity.
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
fn is_keystone(id: String) -> Bool {
|
||
if str_eq(id, "kn-efeb4a5b-5aff-4759-8a97-7233099be6ee") { return true }
|
||
if str_eq(id, "kn-5b606390-a52d-4ca2-8e0e-eba141d13440") { return true }
|
||
return false
|
||
}
|
||
|
||
// membership test in a [String] set
|
||
fn set_has(ids: [String], id: String) -> Bool {
|
||
let n: Int = el_list_len(ids)
|
||
let i: Int = 0
|
||
while i < n {
|
||
if str_eq(el_list_get(ids, i), id) { return true }
|
||
i = i + 1
|
||
}
|
||
return false
|
||
}
|
||
|
||
// ── ISOLATE ────────────────────────────────────────────────────────────────
|
||
// Select the region as a SET: cosine/token retrieval around the vantage
|
||
// (aperture k), optionally unioned with the 1-hop adjacency of each hit.
|
||
// Keystones are excluded from the mutable region by construction.
|
||
fn isolate_region(vantage: String, k: Int, expand: Int) -> [String] {
|
||
let ids: [String] = el_list_empty()
|
||
if str_eq(vantage, "") { return ids }
|
||
// (a) cosine/token retrieval — a clean node array [{"id":..},..]
|
||
let arr: String = engram_search_json(vantage, k)
|
||
let n: Int = json_array_len(arr)
|
||
let i: Int = 0
|
||
while i < n {
|
||
let hit: String = json_array_get(arr, i)
|
||
let id: String = json_get_string(hit, "id")
|
||
if !str_eq(id, "") {
|
||
if !is_keystone(id) {
|
||
if !set_has(ids, id) { ids = el_list_append(ids, id) }
|
||
}
|
||
}
|
||
i = i + 1
|
||
}
|
||
// (b) adjacency: union the 1-hop neighbourhood of each retrieved node.
|
||
// Iterate only over the original cosine seeds [0, seeds); neighbours append
|
||
// past that bound, so this is one hop, not a transitive sweep.
|
||
if expand > 0 {
|
||
let seeds: Int = el_list_len(ids)
|
||
let s: Int = 0
|
||
while s < seeds {
|
||
let seed: String = el_list_get(ids, s)
|
||
let nb: String = engram_neighbors_json(seed, 1, "both")
|
||
let m: Int = json_array_len(nb)
|
||
let j: Int = 0
|
||
while j < m {
|
||
let elem: String = json_array_get(nb, j)
|
||
let nodeobj: String = json_get_raw(elem, "node")
|
||
let nid: String = json_get_string(nodeobj, "id")
|
||
if !str_eq(nid, "") {
|
||
if !is_keystone(nid) {
|
||
if !set_has(ids, nid) { ids = el_list_append(ids, nid) }
|
||
}
|
||
}
|
||
j = j + 1
|
||
}
|
||
s = s + 1
|
||
}
|
||
}
|
||
return ids
|
||
}
|
||
|
||
// ── SUPERSEDE (set) ────────────────────────────────────────────────────────
|
||
// Retire the region AS A WHOLE: one region-tombstone marker carries the
|
||
// provenance (reason + the full superseded id set); every region node is bound
|
||
// to it with a "superseded_by" edge. Originals are RETAINED — immutable
|
||
// tombstone, never a hard delete (engram_forget is deliberately NOT used).
|
||
// Returns the tombstone marker id ("" if the region is empty).
|
||
fn supersede_set(region: [String], reason: String) -> String {
|
||
let n: Int = el_list_len(region)
|
||
if n == 0 { return "" }
|
||
let csv: String = ""
|
||
let i0: Int = 0
|
||
while i0 < n {
|
||
let sep: String = if i0 == 0 { "" } else { "," }
|
||
csv = csv + sep + el_list_get(region, i0)
|
||
i0 = i0 + 1
|
||
}
|
||
let content: String = "region-tombstone: " + reason + " | superseded " + int_to_str(n) + " nodes: " + csv
|
||
let tomb: String = engram_node_full(content, "Tombstone", "region-tombstone", 0.1, 0.1, 1.0, "Episodic", "[\"tombstone\",\"region-supersede\"]")
|
||
let i: Int = 0
|
||
while i < n {
|
||
let rid: String = el_list_get(region, i)
|
||
engram_connect(rid, tomb, 1.0, "superseded_by")
|
||
i = i + 1
|
||
}
|
||
return tomb
|
||
}
|
||
|
||
// ── INSERT (manifold) ──────────────────────────────────────────────────────
|
||
// Insert the new manifold as a SET. Inline JSON array of node objects
|
||
// {content, node_type?, tier?, tags?}. Each becomes a real embedded node
|
||
// (engram_node_full is the n=1 insert atom); the manifold is the set built from
|
||
// those atoms, wired with internal "manifold_member" edges so it enters as one
|
||
// connected sub-graph. Identity node_types (self/values) are demoted to Memory
|
||
// — identity can never be minted through reframe. Returns the new node ids.
|
||
fn insert_manifold_json(manifold: String) -> [String] {
|
||
let out: [String] = el_list_empty()
|
||
if str_eq(manifold, "") { return out }
|
||
let n: Int = json_array_len(manifold)
|
||
if n <= 0 { return out }
|
||
let i: Int = 0
|
||
let prev: String = ""
|
||
while i < n {
|
||
let obj: String = json_array_get(manifold, i)
|
||
let content: String = json_get_string(obj, "content")
|
||
if !str_eq(content, "") {
|
||
let nt_raw: String = json_get_string(obj, "node_type")
|
||
let nt: String = if str_eq(nt_raw, "") { "Memory" } else { nt_raw }
|
||
if str_eq(nt, "self") { nt = "Memory" }
|
||
if str_eq(nt, "values") { nt = "Memory" }
|
||
let tier_raw: String = json_get_string(obj, "tier")
|
||
let tier: String = if str_eq(tier_raw, "") { "Working" } else { tier_raw }
|
||
let tags_raw: String = json_get_raw(obj, "tags")
|
||
let tags: String = if str_eq(tags_raw, "") { "" } else { tags_raw }
|
||
let label: String = str_slice(content, 0, 60)
|
||
let id: String = engram_node_full(content, nt, label, 0.5, 0.5, 0.9, tier, tags)
|
||
out = el_list_append(out, id)
|
||
if !str_eq(prev, "") { engram_connect(prev, id, 0.6, "manifold_member") }
|
||
prev = id
|
||
}
|
||
i = i + 1
|
||
}
|
||
return out
|
||
}
|
||
|
||
// ── REBIND (edges by cosine) ───────────────────────────────────────────────
|
||
// Re-embed the new manifold into the surrounding geometry: bind each new node
|
||
// to the tombstone marker (provenance: new region -reframes-> retired region),
|
||
// then to its top cosine/token neighbours in the store (skipping itself, the
|
||
// new set, keystones, tombstones). Returns the number of edges bound.
|
||
fn rebind_cosine(new_ids: [String], tomb: String) -> Int {
|
||
let bound: Int = 0
|
||
let n: Int = el_list_len(new_ids)
|
||
let i: Int = 0
|
||
while i < n {
|
||
let nid: String = el_list_get(new_ids, i)
|
||
if !str_eq(tomb, "") {
|
||
engram_connect(nid, tomb, 0.8, "reframes")
|
||
bound = bound + 1
|
||
}
|
||
let node_json: String = engram_get_node_json(nid)
|
||
let content: String = json_get_string(node_json, "content")
|
||
let arr: String = engram_search_json(content, 5)
|
||
let m: Int = json_array_len(arr)
|
||
let j: Int = 0
|
||
while j < m {
|
||
let hit: String = json_array_get(arr, j)
|
||
let hid: String = json_get_string(hit, "id")
|
||
if !str_eq(hid, "") {
|
||
if !str_eq(hid, nid) {
|
||
if !is_keystone(hid) {
|
||
if !set_has(new_ids, hid) {
|
||
let htype: String = json_get_string(hit, "node_type")
|
||
if !str_eq(htype, "Tombstone") {
|
||
engram_connect(nid, hid, 0.5, "related")
|
||
bound = bound + 1
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
j = j + 1
|
||
}
|
||
i = i + 1
|
||
}
|
||
return bound
|
||
}
|
||
|
||
// ── THE OPERATION ──────────────────────────────────────────────────────────
|
||
// isolate (done by caller) → supersede region → insert manifold → rebind →
|
||
// one atomic persist → verify report. This is the whole operation; every
|
||
// mutation route below is a projection of it.
|
||
fn reframe_core(region: [String], manifold: String, reason: String, do_rebind: Int) -> String {
|
||
let n_before: Int = engram_node_count()
|
||
let e_before: Int = engram_edge_count()
|
||
let region_n: Int = el_list_len(region)
|
||
let tomb: String = if region_n > 0 { supersede_set(region, reason) } else { "" }
|
||
let new_ids: [String] = insert_manifold_json(manifold)
|
||
let inserted: Int = el_list_len(new_ids)
|
||
let bound: Int = if do_rebind > 0 { rebind_cosine(new_ids, tomb) } else { 0 }
|
||
let saved: Int = persist_canonical()
|
||
let new_csv: String = ""
|
||
let k: Int = 0
|
||
while k < inserted {
|
||
let sep: String = if k == 0 { "" } else { "," }
|
||
new_csv = new_csv + sep + "\"" + el_list_get(new_ids, k) + "\""
|
||
k = k + 1
|
||
}
|
||
return "{\"ok\":true,\"region_superseded\":" + int_to_str(region_n) +
|
||
",\"tombstone_id\":\"" + tomb + "\"" +
|
||
",\"inserted\":" + int_to_str(inserted) +
|
||
",\"new_ids\":[" + new_csv + "]" +
|
||
",\"edges_rebound\":" + int_to_str(bound) +
|
||
",\"nodes_added\":" + int_to_str(engram_node_count() - n_before) +
|
||
",\"edges_added\":" + int_to_str(engram_edge_count() - e_before) +
|
||
",\"node_count\":" + int_to_str(engram_node_count()) +
|
||
",\"edge_count\":" + int_to_str(engram_edge_count()) +
|
||
",\"keystones_protected\":true}"
|
||
}
|
||
|
||
// POST /api/reframe — the universal set-based mutation.
|
||
// Body: {vantage?, region_ids?(csv), k?, expand?, manifold(json array), reason?, rebind?}
|
||
// region_ids (explicit) wins; else cosine-isolate around vantage.
|
||
fn route_reframe(method: String, path: String, body: String) -> String {
|
||
let region_csv: String = json_get_string(body, "region_ids")
|
||
let vantage: String = json_get_string(body, "vantage")
|
||
let region: [String] = el_list_empty()
|
||
if !str_eq(region_csv, "") {
|
||
let parts: [String] = str_split(region_csv, ",")
|
||
let pn: Int = el_list_len(parts)
|
||
let i: Int = 0
|
||
while i < pn {
|
||
let id: String = str_trim(el_list_get(parts, i))
|
||
if !str_eq(id, "") {
|
||
if is_keystone(id) { return err_json("reframe: identity keystone write-protected") }
|
||
if !set_has(region, id) { region = el_list_append(region, id) }
|
||
}
|
||
i = i + 1
|
||
}
|
||
} else {
|
||
if !str_eq(vantage, "") {
|
||
let kv: Int = json_get_int(body, "k")
|
||
let kk: Int = if kv > 0 { kv } else { 12 }
|
||
let expand: Int = json_get_int(body, "expand")
|
||
region = isolate_region(vantage, kk, expand)
|
||
}
|
||
}
|
||
let manifold: String = json_get_raw(body, "manifold")
|
||
let reason_raw: String = json_get_string(body, "reason")
|
||
let reason: String = if str_eq(reason_raw, "") { "reframe" } else { reason_raw }
|
||
// rebind defaults ON for reframe (absent → 1); explicit 0 disables.
|
||
let rebind_raw: String = json_get_raw(body, "rebind")
|
||
let do_rebind: Int = if str_eq(rebind_raw, "") { 1 } else { json_get_int(body, "rebind") }
|
||
return reframe_core(region, manifold, reason, do_rebind)
|
||
}
|
||
|
||
// write — DEGENERATE n=1 of reframe: region=∅, manifold=[1 node]. The SAME
|
||
// reframe_core path. rebind off so the pure-add matches plain node creation.
|
||
// POST /api/write {content, node_type?, tier?, tags?}
|
||
fn route_write(method: String, path: String, body: String) -> String {
|
||
let content: String = json_get_string(body, "content")
|
||
if str_eq(content, "") { return err_json("write: content required") }
|
||
let nt: String = json_get_string(body, "node_type")
|
||
if str_eq(nt, "self") { return err_json("write: identity is write-protected") }
|
||
if str_eq(nt, "values") { return err_json("write: identity is write-protected") }
|
||
let empty: [String] = el_list_empty()
|
||
let manifold: String = "[" + body + "]" // the body IS a valid manifold node object
|
||
return reframe_core(empty, manifold, "write", 0)
|
||
}
|
||
|
||
// supersede — DEGENERATE n=1 of reframe: region={id}, manifold=[1 node]. The
|
||
// SAME reframe_core path with a size-1 region. Original retained (immutable);
|
||
// new node inserted and cosine-rebound; provenance edge new-reframes-tomb.
|
||
// POST /api/supersede {id, content, node_type?, tier?, tags?, reason?}
|
||
fn route_supersede(method: String, path: String, body: String) -> String {
|
||
let id: String = json_get_string(body, "id")
|
||
if str_eq(id, "") { return err_json("supersede: id required") }
|
||
if is_keystone(id) { return err_json("supersede: identity keystone write-protected") }
|
||
let content: String = json_get_string(body, "content")
|
||
if str_eq(content, "") { return err_json("supersede: content required") }
|
||
let region: [String] = el_list_empty()
|
||
region = el_list_append(region, id)
|
||
let manifold: String = "[" + body + "]"
|
||
let reason_raw: String = json_get_string(body, "reason")
|
||
let reason: String = if str_eq(reason_raw, "") { "supersede " + id } else { reason_raw }
|
||
return reframe_core(region, manifold, reason, 1)
|
||
}
|
||
|
||
// ── Auth ──────────────────────────────────────────────────────────────────────
|
||
|
||
fn check_auth_ok(method: String, body: String) -> Bool {
|
||
let key: String = env("ENGRAM_API_KEY")
|
||
if str_eq(key, "") { return true }
|
||
// Read-only methods don't require auth. Until http_serve surfaces
|
||
// request headers we can't accept a Bearer token cleanly; mutating
|
||
// requests must include "_auth": "<key>" in the JSON body.
|
||
if str_eq(method, "GET") { return true }
|
||
let provided: String = json_get_string(body, "_auth")
|
||
if str_eq(provided, key) { return true }
|
||
return false
|
||
}
|
||
|
||
// ── Dispatcher ────────────────────────────────────────────────────────────────
|
||
|
||
fn handle_request(method: String, path: String, body: String) -> String {
|
||
let clean: String = strip_query(path)
|
||
|
||
// Health is always reachable
|
||
if str_eq(method, "GET") {
|
||
if str_eq(clean, "/health") || str_eq(clean, "/") {
|
||
return route_health(method, path, body)
|
||
}
|
||
}
|
||
|
||
// ISE posting is auth-exempt (internal soul daemon, same host, no _auth key)
|
||
if str_eq(method, "POST") && str_eq(clean, "/api/neuron/state-events") {
|
||
return route_emit_ise(method, path, body)
|
||
}
|
||
|
||
// Auth (when ENGRAM_API_KEY is set)
|
||
if !check_auth_ok(method, body) {
|
||
return err_json("unauthorized")
|
||
}
|
||
|
||
// Knowledge capture (auth enforced above; the world-ingestor integrator
|
||
// and any headless session without MCP push knowledge through this)
|
||
if str_eq(method, "POST") && str_eq(clean, "/api/neuron/knowledge/capture") {
|
||
return route_capture_knowledge(method, path, body)
|
||
}
|
||
|
||
// Stats
|
||
if str_eq(method, "GET") && (str_eq(clean, "/api/stats") || str_eq(clean, "/stats")) {
|
||
return route_stats(method, path, body)
|
||
}
|
||
|
||
// ── The universal set-based operation and its n=1 degenerate projections ──
|
||
// reframe = isolate → supersede-region → insert-manifold → rebind. write and
|
||
// supersede are the SAME reframe_core path at region size 0 and 1.
|
||
if str_eq(method, "POST") && (str_eq(clean, "/api/reframe") || str_eq(clean, "/reframe")) {
|
||
return route_reframe(method, path, body)
|
||
}
|
||
if str_eq(method, "POST") && (str_eq(clean, "/api/write") || str_eq(clean, "/write")) {
|
||
return route_write(method, path, body)
|
||
}
|
||
if str_eq(method, "POST") && (str_eq(clean, "/api/supersede") || str_eq(clean, "/supersede")) {
|
||
return route_supersede(method, path, body)
|
||
}
|
||
|
||
// Nodes
|
||
if str_eq(method, "POST") && (str_eq(clean, "/api/nodes") || str_eq(clean, "/nodes")) {
|
||
return route_create_node(method, path, body)
|
||
}
|
||
if str_eq(method, "GET") && (str_eq(clean, "/api/nodes") || str_eq(clean, "/nodes") || str_eq(clean, "/nodes/list") || str_eq(clean, "/api/nodes/list")) {
|
||
return route_scan_nodes(method, path, body)
|
||
}
|
||
if str_eq(method, "GET") && (str_eq(clean, "/api/edges") || str_eq(clean, "/edges")) {
|
||
return route_scan_edges(method, path, body)
|
||
}
|
||
if str_eq(method, "GET") && str_starts_with(clean, "/api/nodes/") {
|
||
return route_get_node(method, path, body)
|
||
}
|
||
if str_eq(method, "DELETE") && str_starts_with(clean, "/api/nodes/") {
|
||
return route_forget(method, path, body)
|
||
}
|
||
|
||
// Edges
|
||
if str_eq(method, "POST") && (str_eq(clean, "/api/edges") || str_eq(clean, "/edges")) {
|
||
return route_create_edge(method, path, body)
|
||
}
|
||
if str_eq(method, "GET") && str_starts_with(clean, "/api/neighbors/") {
|
||
return route_neighbors(method, path, body)
|
||
}
|
||
|
||
// Activation + Search
|
||
if str_eq(method, "POST") && (str_eq(clean, "/api/activate") || str_eq(clean, "/activate")) {
|
||
return route_activate(method, path, body)
|
||
}
|
||
if str_eq(method, "GET") && str_starts_with(clean, "/api/activate") {
|
||
return route_activate(method, path, body)
|
||
}
|
||
if str_eq(method, "POST") && (str_eq(clean, "/api/search") || str_eq(clean, "/search")) {
|
||
return route_search(method, path, body)
|
||
}
|
||
if str_eq(method, "GET") && str_starts_with(clean, "/api/search") {
|
||
return route_search(method, path, body)
|
||
}
|
||
|
||
// Strengthen
|
||
if str_eq(method, "POST") && (str_eq(clean, "/api/strengthen") || str_eq(clean, "/strengthen")) {
|
||
return route_strengthen(method, path, body)
|
||
}
|
||
|
||
// Persistence
|
||
if str_eq(method, "POST") && (str_eq(clean, "/api/save") || str_eq(clean, "/save")) {
|
||
return route_save(method, path, body)
|
||
}
|
||
if str_eq(method, "POST") && (str_eq(clean, "/api/load") || str_eq(clean, "/load")) {
|
||
return route_load(method, path, body)
|
||
}
|
||
if str_eq(method, "POST") && (str_eq(clean, "/api/load-merge") || str_eq(clean, "/load-merge")) {
|
||
return route_load_merge(method, path, body)
|
||
}
|
||
|
||
// Sync — soul daemon periodic pull of non-ISE knowledge into in-process graph
|
||
if str_eq(method, "GET") && str_eq(clean, "/api/sync") {
|
||
return route_sync(method, path, body)
|
||
}
|
||
|
||
"{\"error\":\"not found\",\"path\":\"" + clean + "\"}"
|
||
}
|
||
|
||
// ── Entry ─────────────────────────────────────────────────────────────────────
|
||
|
||
let bind_raw: String = env("ENGRAM_BIND")
|
||
let bind_str: String = if str_eq(bind_raw, "") { ":8742" } else { bind_raw }
|
||
let port: Int = parse_port(bind_str)
|
||
|
||
// On startup, try to load any existing snapshot (best effort).
|
||
let data_dir_raw: String = env("ENGRAM_DATA_DIR")
|
||
let data_dir: String = if str_eq(data_dir_raw, "") { "/tmp/engram" } else { data_dir_raw }
|
||
let snapshot_path: String = data_dir + "/snapshot.json"
|
||
engram_load(snapshot_path)
|
||
|
||
// 2026-07-21 self-review boot guard: if the snapshot file has content but the
|
||
// load produced 0 nodes, something is wrong (corrupt file / parse failure).
|
||
// Preserve the evidence and warn loudly — and since read routes no longer write
|
||
// the canonical path, a bad boot can no longer clobber the good snapshot.
|
||
let boot_snap: String = fs_read(snapshot_path)
|
||
if !str_eq(boot_snap, "") {
|
||
if engram_node_count() == 0 {
|
||
println("[engram] WARNING: snapshot.json is non-empty but load produced 0 nodes — preserving copy at snapshot.failed-load.json")
|
||
fs_write(data_dir + "/snapshot.failed-load.json", boot_snap)
|
||
} else {
|
||
// Good load: keep a boot-time backup of the snapshot as loaded.
|
||
fs_write(data_dir + "/snapshot.boot-backup.json", boot_snap)
|
||
}
|
||
}
|
||
|
||
println("[engram] runtime-native graph engine")
|
||
println("[engram] data_dir=" + data_dir)
|
||
println("[engram] node_count=" + int_to_str(engram_node_count()))
|
||
println("[engram] edge_count=" + int_to_str(engram_edge_count()))
|
||
println("[engram] listening on " + int_to_str(port))
|
||
|
||
http_set_handler("handle_request")
|
||
http_serve(port, "handle_request")
|