self-review 2026-07-24: wire embedding cosine similarity into activation (bl-b2d1c944)

Semantic activation was spec-only since 2026-06-30 — the seed loop used
istr_contains and nothing else. Per the 07-21 integration brief:

- EngramNode gains a lazily-backfilled nomic-embed-text vector (8/call
  inside engram_activate, newest-first; no create-path latency, no bulk
  Ollama hammering during sync seeds)
- query embedding (cached) drives a top-K cosine seed supplement
  (HippoRAG use-similarity-twice) plus an additive WM term with
  shift-and-floor at 0.45 — raw cosine is a constant bias in anisotropic
  spaces (unrelated pairs read 0.4-0.7), floor-and-ramp makes it a signal
- 4s embed timeout (http_do_t) + 3-strike circuit breaker: activation
  never wedges on a dead embedder; everything degrades to lexical
- embeddings persist as %.4g comma lists in snapshots, parsed by both
  loaders; embedded_count in /api/stats tracks coverage
- engram_cosine_sim + http_delete_json exposed (DELETE now carries a
  body — the server's _auth scheme requires it)
- route_create_node honored only content/node_type/salience; label,
  importance, tier, tags were silently dropped (label defaulted to
  content). Now honored via engram_node_full.

Verified live: embedded_count 0->96 across activations, semantic-only
promotion observed (zero token overlap), snapshot round-trip intact.
This commit is contained in:
2026-07-24 08:52:54 -05:00
parent 8f8ccc945e
commit 97e484221d
5 changed files with 415 additions and 38 deletions
BIN
View File
Binary file not shown.
+31 -24
View File
@@ -130,7 +130,14 @@ el_val_t route_create_node(el_val_t method, el_val_t path, el_val_t body) {
el_val_t node_type = ({ el_val_t _if_result_2 = 0; if (str_eq(nt_raw, EL_STR(""))) { _if_result_2 = (EL_STR("Memory")); } else { _if_result_2 = (nt_raw); } _if_result_2; });
el_val_t sal_raw = json_get_float(body, EL_STR("salience"));
el_val_t salience = ({ el_val_t _if_result_3 = 0; if ((sal_raw == el_from_float(0.0))) { _if_result_3 = (el_from_float(0.5)); } else { _if_result_3 = (sal_raw); } _if_result_3; });
el_val_t id = engram_node(content, node_type, salience);
el_val_t label_raw = json_get_string(body, EL_STR("label"));
el_val_t label = ({ el_val_t _if_result_4 = 0; if (str_eq(label_raw, EL_STR(""))) { _if_result_4 = (content); } else { _if_result_4 = (label_raw); } _if_result_4; });
el_val_t imp_raw = json_get_float(body, EL_STR("importance"));
el_val_t importance = ({ el_val_t _if_result_5 = 0; if ((imp_raw == el_from_float(0.0))) { _if_result_5 = (el_from_float(0.5)); } else { _if_result_5 = (imp_raw); } _if_result_5; });
el_val_t tier_raw = json_get_string(body, EL_STR("tier"));
el_val_t tier = ({ el_val_t _if_result_6 = 0; if (str_eq(tier_raw, EL_STR(""))) { _if_result_6 = (EL_STR("Working")); } else { _if_result_6 = (tier_raw); } _if_result_6; });
el_val_t tags = json_get_string(body, EL_STR("tags"));
el_val_t id = engram_node_full(content, node_type, label, el_from_float(salience), el_from_float(importance), el_from_float(1.0), tier, tags);
el_val_t saved = persist_canonical();
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"id\":\""), id), EL_STR("\",\"content\":\"")), content), EL_STR("\",\"node_type\":\"")), node_type), EL_STR("\"}"));
return 0;
@@ -158,7 +165,7 @@ el_val_t route_scan_nodes(el_val_t method, el_val_t path, el_val_t body) {
el_val_t route_scan_edges(el_val_t method, el_val_t path, el_val_t body) {
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
el_val_t dir = ({ el_val_t _if_result_4 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_4 = (EL_STR("/tmp/engram")); } else { _if_result_4 = (dir_raw); } _if_result_4; });
el_val_t dir = ({ el_val_t _if_result_7 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_7 = (EL_STR("/tmp/engram")); } else { _if_result_7 = (dir_raw); } _if_result_7; });
el_val_t snap_path = el_str_concat(dir, EL_STR("/.scan-export.json"));
engram_save(snap_path);
el_val_t snap = fs_read(snap_path);
@@ -174,22 +181,22 @@ el_val_t route_scan_edges(el_val_t method, el_val_t path, el_val_t body) {
}
el_val_t route_search(el_val_t method, el_val_t path, el_val_t body) {
el_val_t q = ({ el_val_t _if_result_5 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_5 = (query_param(path, EL_STR("q"))); } else { _if_result_5 = (json_get_string(body, EL_STR("query"))); } _if_result_5; });
el_val_t q = ({ el_val_t _if_result_8 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_8 = (query_param(path, EL_STR("q"))); } else { _if_result_8 = (json_get_string(body, EL_STR("query"))); } _if_result_8; });
el_val_t lim_url = query_int(path, EL_STR("limit"), 0);
el_val_t lim_body = json_get_int(body, EL_STR("limit"));
el_val_t lim_either = ({ el_val_t _if_result_6 = 0; if ((lim_url > 0)) { _if_result_6 = (lim_url); } else { _if_result_6 = (lim_body); } _if_result_6; });
el_val_t limit = ({ el_val_t _if_result_7 = 0; if ((lim_either > 0)) { _if_result_7 = (lim_either); } else { _if_result_7 = (20); } _if_result_7; });
el_val_t lim_either = ({ el_val_t _if_result_9 = 0; if ((lim_url > 0)) { _if_result_9 = (lim_url); } else { _if_result_9 = (lim_body); } _if_result_9; });
el_val_t limit = ({ el_val_t _if_result_10 = 0; if ((lim_either > 0)) { _if_result_10 = (lim_either); } else { _if_result_10 = (20); } _if_result_10; });
return engram_search_json(q, limit);
return 0;
}
el_val_t route_activate(el_val_t method, el_val_t path, el_val_t body) {
el_val_t q = ({ el_val_t _if_result_8 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_8 = (query_param(path, EL_STR("q"))); } else { _if_result_8 = (json_get_string(body, EL_STR("query"))); } _if_result_8; });
el_val_t q = ({ el_val_t _if_result_11 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_11 = (query_param(path, EL_STR("q"))); } else { _if_result_11 = (json_get_string(body, EL_STR("query"))); } _if_result_11; });
if (str_eq(q, EL_STR(""))) {
return err_json(EL_STR("missing query"));
}
el_val_t d_raw = ({ el_val_t _if_result_9 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_9 = (query_int(path, EL_STR("depth"), 3)); } else { _if_result_9 = (json_get_int(body, EL_STR("depth"))); } _if_result_9; });
el_val_t depth = ({ el_val_t _if_result_10 = 0; if ((d_raw > 0)) { _if_result_10 = (d_raw); } else { _if_result_10 = (3); } _if_result_10; });
el_val_t d_raw = ({ el_val_t _if_result_12 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_12 = (query_int(path, EL_STR("depth"), 3)); } else { _if_result_12 = (json_get_int(body, EL_STR("depth"))); } _if_result_12; });
el_val_t depth = ({ el_val_t _if_result_13 = 0; if ((d_raw > 0)) { _if_result_13 = (d_raw); } else { _if_result_13 = (3); } _if_result_13; });
return el_str_concat(el_str_concat(EL_STR("{\"results\":"), engram_activate_json(q, depth)), EL_STR("}"));
return 0;
}
@@ -198,9 +205,9 @@ el_val_t route_create_edge(el_val_t method, el_val_t path, el_val_t body) {
el_val_t from_id = json_get_string(body, EL_STR("from_id"));
el_val_t to_id = json_get_string(body, EL_STR("to_id"));
el_val_t rel_raw = json_get_string(body, EL_STR("relation"));
el_val_t relation = ({ el_val_t _if_result_11 = 0; if (str_eq(rel_raw, EL_STR(""))) { _if_result_11 = (EL_STR("associates")); } else { _if_result_11 = (rel_raw); } _if_result_11; });
el_val_t relation = ({ el_val_t _if_result_14 = 0; if (str_eq(rel_raw, EL_STR(""))) { _if_result_14 = (EL_STR("associates")); } else { _if_result_14 = (rel_raw); } _if_result_14; });
el_val_t w_raw = json_get_float(body, EL_STR("weight"));
el_val_t weight = ({ el_val_t _if_result_12 = 0; if ((w_raw == el_from_float(0.0))) { _if_result_12 = (el_from_float(0.5)); } else { _if_result_12 = (w_raw); } _if_result_12; });
el_val_t weight = ({ el_val_t _if_result_15 = 0; if ((w_raw == el_from_float(0.0))) { _if_result_15 = (el_from_float(0.5)); } else { _if_result_15 = (w_raw); } _if_result_15; });
engram_connect(from_id, to_id, weight, relation);
el_val_t saved = persist_canonical();
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"from_id\":\""), from_id), EL_STR("\",\"to_id\":\"")), to_id), EL_STR("\",\"relation\":\"")), relation), EL_STR("\"}"));
@@ -242,8 +249,8 @@ el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body) {
el_val_t route_save(el_val_t method, el_val_t path, el_val_t body) {
el_val_t p_raw = json_get_string(body, EL_STR("path"));
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
el_val_t dir = ({ el_val_t _if_result_13 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_13 = (EL_STR("/tmp/engram")); } else { _if_result_13 = (dir_raw); } _if_result_13; });
el_val_t p = ({ el_val_t _if_result_14 = 0; if (str_eq(p_raw, EL_STR(""))) { _if_result_14 = (el_str_concat(dir, EL_STR("/snapshot.json"))); } else { _if_result_14 = (p_raw); } _if_result_14; });
el_val_t dir = ({ el_val_t _if_result_16 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_16 = (EL_STR("/tmp/engram")); } else { _if_result_16 = (dir_raw); } _if_result_16; });
el_val_t p = ({ el_val_t _if_result_17 = 0; if (str_eq(p_raw, EL_STR(""))) { _if_result_17 = (el_str_concat(dir, EL_STR("/snapshot.json"))); } else { _if_result_17 = (p_raw); } _if_result_17; });
engram_save(p);
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"path\":\""), p), EL_STR("\"}"));
return 0;
@@ -252,8 +259,8 @@ el_val_t route_save(el_val_t method, el_val_t path, el_val_t body) {
el_val_t route_load(el_val_t method, el_val_t path, el_val_t body) {
el_val_t p_raw = json_get_string(body, EL_STR("path"));
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
el_val_t dir = ({ el_val_t _if_result_15 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_15 = (EL_STR("/tmp/engram")); } else { _if_result_15 = (dir_raw); } _if_result_15; });
el_val_t p = ({ el_val_t _if_result_16 = 0; if (str_eq(p_raw, EL_STR(""))) { _if_result_16 = (el_str_concat(dir, EL_STR("/snapshot.json"))); } else { _if_result_16 = (p_raw); } _if_result_16; });
el_val_t dir = ({ el_val_t _if_result_18 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_18 = (EL_STR("/tmp/engram")); } else { _if_result_18 = (dir_raw); } _if_result_18; });
el_val_t p = ({ el_val_t _if_result_19 = 0; if (str_eq(p_raw, EL_STR(""))) { _if_result_19 = (el_str_concat(dir, EL_STR("/snapshot.json"))); } else { _if_result_19 = (p_raw); } _if_result_19; });
engram_load(p);
return ok_json();
return 0;
@@ -266,7 +273,7 @@ el_val_t route_health(el_val_t method, el_val_t path, el_val_t body) {
el_val_t route_sync(el_val_t method, el_val_t path, el_val_t body) {
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
el_val_t dir = ({ el_val_t _if_result_17 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_17 = (EL_STR("/tmp/engram")); } else { _if_result_17 = (dir_raw); } _if_result_17; });
el_val_t dir = ({ el_val_t _if_result_20 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_20 = (EL_STR("/tmp/engram")); } else { _if_result_20 = (dir_raw); } _if_result_20; });
el_val_t snap_path = el_str_concat(dir, EL_STR("/.sync-export.json"));
engram_save(snap_path);
el_val_t snap = fs_read(snap_path);
@@ -305,7 +312,7 @@ el_val_t route_emit_ise(el_val_t method, el_val_t path, el_val_t body) {
el_val_t conf = el_from_float(0.8);
el_val_t id = engram_node_full(content, EL_STR("InternalStateEvent"), EL_STR("state-event"), sal, imp, conf, EL_STR("Episodic"), EL_STR("[\"internal-state\",\"InternalStateEvent\"]"));
el_val_t ret_raw = env(EL_STR("ENGRAM_ISE_RETENTION_MS"));
el_val_t ret_ms = ({ el_val_t _if_result_18 = 0; if (str_eq(ret_raw, EL_STR(""))) { _if_result_18 = (172800000); } else { _if_result_18 = (str_to_int(ret_raw)); } _if_result_18; });
el_val_t ret_ms = ({ el_val_t _if_result_21 = 0; if (str_eq(ret_raw, EL_STR(""))) { _if_result_21 = (172800000); } else { _if_result_21 = (str_to_int(ret_raw)); } _if_result_21; });
el_val_t pruned = engram_prune_telemetry(ret_ms);
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), id), EL_STR("\",\"pruned\":")), int_to_str(pruned)), EL_STR("}"));
return 0;
@@ -317,21 +324,21 @@ el_val_t route_capture_knowledge(el_val_t method, el_val_t path, el_val_t body)
return err_json(EL_STR("missing content"));
}
el_val_t title = json_get_string(body, EL_STR("title"));
el_val_t label = ({ el_val_t _if_result_19 = 0; if (str_eq(title, EL_STR(""))) { _if_result_19 = (str_slice(content, 0, 60)); } else { _if_result_19 = (title); } _if_result_19; });
el_val_t label = ({ el_val_t _if_result_22 = 0; if (str_eq(title, EL_STR(""))) { _if_result_22 = (str_slice(content, 0, 60)); } else { _if_result_22 = (title); } _if_result_22; });
el_val_t category_raw = json_get_string(body, EL_STR("category"));
el_val_t category = ({ el_val_t _if_result_20 = 0; if (str_eq(category_raw, EL_STR(""))) { _if_result_20 = (EL_STR("other")); } else { _if_result_20 = (category_raw); } _if_result_20; });
el_val_t category = ({ el_val_t _if_result_23 = 0; if (str_eq(category_raw, EL_STR(""))) { _if_result_23 = (EL_STR("other")); } else { _if_result_23 = (category_raw); } _if_result_23; });
el_val_t ktier_raw = json_get_string(body, EL_STR("tier"));
el_val_t ktier = ({ el_val_t _if_result_21 = 0; if (str_eq(ktier_raw, EL_STR(""))) { _if_result_21 = (EL_STR("note")); } else { _if_result_21 = (ktier_raw); } _if_result_21; });
el_val_t ktier = ({ el_val_t _if_result_24 = 0; if (str_eq(ktier_raw, EL_STR(""))) { _if_result_24 = (EL_STR("note")); } else { _if_result_24 = (ktier_raw); } _if_result_24; });
el_val_t project = json_get_string(body, EL_STR("project"));
el_val_t tags_raw = json_get_raw(body, EL_STR("tags"));
el_val_t tags_base = ({ el_val_t _if_result_22 = 0; if (str_eq(tags_raw, EL_STR(""))) { _if_result_22 = (EL_STR("[]")); } else { _if_result_22 = (tags_raw); } _if_result_22; });
el_val_t tags_base = ({ el_val_t _if_result_25 = 0; if (str_eq(tags_raw, EL_STR(""))) { _if_result_25 = (EL_STR("[]")); } else { _if_result_25 = (tags_raw); } _if_result_25; });
el_val_t base_len = str_len(tags_base);
el_val_t head = str_slice(tags_base, 0, (base_len - 1));
el_val_t sep = ({ el_val_t _if_result_23 = 0; if (str_eq(head, EL_STR("["))) { _if_result_23 = (EL_STR("")); } else { _if_result_23 = (EL_STR(",")); } _if_result_23; });
el_val_t sep = ({ el_val_t _if_result_26 = 0; if (str_eq(head, EL_STR("["))) { _if_result_26 = (EL_STR("")); } else { _if_result_26 = (EL_STR(",")); } _if_result_26; });
el_val_t safe_cat = str_replace(category, EL_STR("\""), EL_STR("'"));
el_val_t safe_tier = str_replace(ktier, EL_STR("\""), EL_STR("'"));
el_val_t safe_proj = str_replace(project, EL_STR("\""), EL_STR("'"));
el_val_t proj_tag = ({ el_val_t _if_result_24 = 0; if (str_eq(safe_proj, EL_STR(""))) { _if_result_24 = (EL_STR("")); } else { _if_result_24 = (el_str_concat(el_str_concat(EL_STR(",\"project:"), safe_proj), EL_STR("\""))); } _if_result_24; });
el_val_t proj_tag = ({ el_val_t _if_result_27 = 0; if (str_eq(safe_proj, EL_STR(""))) { _if_result_27 = (EL_STR("")); } else { _if_result_27 = (el_str_concat(el_str_concat(EL_STR(",\"project:"), safe_proj), EL_STR("\""))); } _if_result_27; });
el_val_t tags = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(head, sep), EL_STR("\"category:")), safe_cat), EL_STR("\",\"tier:")), safe_tier), EL_STR("\"")), proj_tag), EL_STR("]"));
el_val_t sal = el_from_float(0.5);
el_val_t imp = el_from_float(0.5);
@@ -432,10 +439,10 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
int main(int _argc, char** _argv) {
el_runtime_init_args(_argc, _argv);
bind_raw = env(EL_STR("ENGRAM_BIND"));
bind_str = ({ el_val_t _if_result_25 = 0; if (str_eq(bind_raw, EL_STR(""))) { _if_result_25 = (EL_STR(":8742")); } else { _if_result_25 = (bind_raw); } _if_result_25; });
bind_str = ({ el_val_t _if_result_28 = 0; if (str_eq(bind_raw, EL_STR(""))) { _if_result_28 = (EL_STR(":8742")); } else { _if_result_28 = (bind_raw); } _if_result_28; });
port = parse_port(bind_str);
data_dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
data_dir = ({ el_val_t _if_result_26 = 0; if (str_eq(data_dir_raw, EL_STR(""))) { _if_result_26 = (EL_STR("/tmp/engram")); } else { _if_result_26 = (data_dir_raw); } _if_result_26; });
data_dir = ({ el_val_t _if_result_29 = 0; if (str_eq(data_dir_raw, EL_STR(""))) { _if_result_29 = (EL_STR("/tmp/engram")); } else { _if_result_29 = (data_dir_raw); } _if_result_29; });
snapshot_path = el_str_concat(data_dir, EL_STR("/snapshot.json"));
engram_load(snapshot_path);
boot_snap = fs_read(snapshot_path);
+18 -1
View File
@@ -105,13 +105,30 @@ fn persist_canonical() -> Int {
return 1
}
// INCOMPLETE-ROUTE FIX (2026-07-24 self-review): this route silently dropped
// label, importance, tier, and tags engram_node() defaults label to content
// and importance to 0.5, so every node created over HTTP lost its metadata.
// Observed live: the soul's boot-counter write-back landed with
// label="soul:boot_count:99" (content), importance 0.5, no tags. Honor the
// full field set via engram_node_full when any of them is supplied.
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 label_raw: String = json_get_string(body, "label")
let label: String = if str_eq(label_raw, "") { content } else { label_raw }
let imp_raw: Float = json_get_float(body, "importance")
let importance: Float = if imp_raw == 0.0 { 0.5 } else { imp_raw }
let tier_raw: String = json_get_string(body, "tier")
let tier: String = if str_eq(tier_raw, "") { "Working" } else { tier_raw }
let tags: String = json_get_string(body, "tags")
let id: String = engram_node_full(
content, node_type, label,
el_from_float(salience), el_from_float(importance), el_from_float(1.0),
tier, tags
)
let saved: Int = persist_canonical()
"{\"id\":\"" + id + "\",\"content\":\"" + content + "\",\"node_type\":\"" + node_type + "\"}"
}
+348 -13
View File
@@ -788,9 +788,12 @@ static long el_http_timeout_ms(void) {
return parsed;
}
/* Internal: do a libcurl request; takes optional body/headers, optional method override. */
static el_val_t http_do(const char* method, const char* url, const char* body,
struct curl_slist* extra_headers) {
/* Internal: do a libcurl request; takes optional body/headers, optional method
* override, and an optional timeout override (0 = use EL_HTTP_TIMEOUT_MS).
* The override exists for the embedding path: activation must never wait the
* full 60s default on a wedged Ollama. (2026-07-24 self-review) */
static el_val_t http_do_t(const char* method, const char* url, const char* body,
struct curl_slist* extra_headers, long timeout_ms) {
if (!url || !*url) return http_error_json("empty url");
CURL* c = curl_easy_init();
if (!c) return http_error_json("curl_easy_init failed");
@@ -800,7 +803,8 @@ static el_val_t http_do(const char* method, const char* url, const char* body,
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb);
curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb);
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms());
curl_easy_setopt(c, CURLOPT_TIMEOUT_MS,
timeout_ms > 0 ? timeout_ms : el_http_timeout_ms());
curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf);
curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0");
@@ -811,6 +815,13 @@ static el_val_t http_do(const char* method, const char* url, const char* body,
curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0));
} else if (method && strcmp(method, "DELETE") == 0) {
curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "DELETE");
/* DELETE with a body (2026-07-24): the engram server authenticates
* mutating requests via an "_auth" field in the JSON body, so EL
* code must be able to send DELETE + body. Absent body unchanged. */
if (body && *body) {
curl_easy_setopt(c, CURLOPT_POSTFIELDS, body);
curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(body));
}
}
CURLcode rc = curl_easy_perform(c);
curl_easy_cleanup(c);
@@ -822,6 +833,12 @@ static el_val_t http_do(const char* method, const char* url, const char* body,
return el_wrap_str(rb.data);
}
/* Legacy entry point: default timeout. */
static el_val_t http_do(const char* method, const char* url, const char* body,
struct curl_slist* extra_headers) {
return http_do_t(method, url, body, extra_headers, 0);
}
el_val_t http_get(el_val_t url) {
return http_do("GET", EL_CSTR(url), NULL, NULL);
}
@@ -895,6 +912,16 @@ el_val_t http_delete(el_val_t url) {
return http_do("DELETE", EL_CSTR(url), NULL, NULL);
}
/* DELETE with a JSON body — required by the engram server's body-based
* "_auth" scheme for mutating requests. (2026-07-24 self-review) */
el_val_t http_delete_json(el_val_t url, el_val_t json_body) {
struct curl_slist* h = NULL;
h = curl_slist_append(h, "Content-Type: application/json");
el_val_t r = http_do("DELETE", EL_CSTR(url), EL_CSTR(json_body), h);
curl_slist_free_all(h);
return r;
}
/* ── HTTP → file streaming ────────────────────────────────────────────────
*
* Why this exists: el_val_t strings are NUL-terminated by convention, so
@@ -5801,6 +5828,14 @@ typedef struct EngramNode {
int32_t access_head;
int32_t access_filled;
double wm_anchor;
/* Semantic embedding (2026-07-24 self-review, bl-b2d1c944).
* emb: malloc'd float vector (nomic-embed-text, 768-dim) or NULL.
* emb_dim: vector length; 0 = not embedded. Populated lazily by the
* backfill pass in engram_activate NOT on the node-create hot path,
* so bulk sync imports never block on Ollama. Freed in engram_forget /
* engram_prune_telemetry; shift-copies move the pointer intact. */
float* emb;
int32_t emb_dim;
} EngramNode;
/* Record an access (ACT-R "presentation") into the base-level ring buffer. */
@@ -5858,6 +5893,159 @@ static void engram_bll_parse_access(EngramNode* nn, const char* s) {
}
}
/* ── Embedding-based activation (2026-07-24 self-review, bl-b2d1c944) ──────
* Closes the gap named in every heartbeat since 2026-06-30: "no embedding
* call is made during activation. The seed-finding loop uses istr_contains
* only." Design per the 2026-07-21 integration brief:
* - Lazy backfill: engram_activate embeds up to BACKFILL_PER_CALL
* un-embedded, non-telemetry nodes per call, newest first. No latency
* on node-create paths; a fresh node is embedded within ~1 scan cycle.
* - Semantic seeding (HippoRAG pattern, use similarity twice): the query
* is embedded, the top-K nodes by cosine SEED_MIN join the seed set
* with initial activation = the similarity itself.
* - Additive WM term: raw_wm += W * relu((cos S0)/(1 S0)). Shift-and-
* floor at S0 because nomic-embed scores unrelated pairs 0.40.5; raw
* cosine in a weighted sum is a constant bias, not a signal.
* - Circuit breaker: 3 consecutive Ollama failures stop trying for 5
* minutes. Activation NEVER blocks on a dead embedder beyond timeout.
*/
#define ENGRAM_EMBED_S0 0.45
#define ENGRAM_EMBED_WM_WEIGHT 0.20
#define ENGRAM_EMBED_SEED_K 8
#define ENGRAM_EMBED_SEED_MIN 0.60
#define ENGRAM_EMBED_BACKFILL_PER_CALL 8
#define ENGRAM_EMBED_MAX_CHARS 2000
#define ENGRAM_EMBED_TIMEOUT_MS 4000L
#define ENGRAM_EMBED_BREAKER_LIMIT 3
#define ENGRAM_EMBED_BREAKER_COOLDOWN_MS 300000
static int _eg_embed_consec_fail = 0;
static int64_t _eg_embed_breaker_until = 0;
static int64_t engram_now_ms(void); /* defined in the store section below */
static const char* eg_embed_url(void) {
const char* s = getenv("EL_EMBED_URL");
return (s && *s) ? s : "http://localhost:11434/api/embeddings";
}
static const char* eg_embed_model(void) {
const char* s = getenv("EL_EMBED_MODEL");
return (s && *s) ? s : "nomic-embed-text";
}
/* Cosine similarity over raw (unnormalized — nomic emits magnitudes >1)
* float vectors. Returns -2.0 on dim mismatch / null input so callers can
* distinguish "orthogonal" (0.0) from "not comparable". */
static double eg_cosine(const float* a, const float* b, int32_t dim) {
if (!a || !b || dim <= 0) return -2.0;
double dot = 0.0, na = 0.0, nb = 0.0;
for (int32_t i = 0; i < dim; i++) {
dot += (double)a[i] * (double)b[i];
na += (double)a[i] * (double)a[i];
nb += (double)b[i] * (double)b[i];
}
if (na <= 0.0 || nb <= 0.0) return -2.0;
return dot / (sqrt(na) * sqrt(nb));
}
/* Fetch an embedding from Ollama. Returns malloc'd float[dim] or NULL.
* Truncates input to ENGRAM_EMBED_MAX_CHARS and JSON-escapes it. Honors the
* circuit breaker; a NULL return is always safe to ignore (fail-soft). */
static float* eg_embed_fetch(const char* text, int32_t* out_dim) {
*out_dim = 0;
if (!text || !*text) return NULL;
int64_t now = engram_now_ms();
if (now < _eg_embed_breaker_until) return NULL;
/* Build request body with escaped, truncated prompt. */
size_t tlen = strlen(text);
if (tlen > ENGRAM_EMBED_MAX_CHARS) tlen = ENGRAM_EMBED_MAX_CHARS;
char* esc = malloc(tlen * 6 + 1);
if (!esc) return NULL;
size_t w = 0;
for (size_t i = 0; i < tlen; i++) {
unsigned char c = (unsigned char)text[i];
if (c == '"' || c == '\\') { esc[w++] = '\\'; esc[w++] = (char)c; }
else if (c == '\n') { esc[w++] = '\\'; esc[w++] = 'n'; }
else if (c == '\r') { esc[w++] = '\\'; esc[w++] = 'r'; }
else if (c == '\t') { esc[w++] = '\\'; esc[w++] = 't'; }
else if (c < 0x20) { w += (size_t)snprintf(esc + w, 7, "\\u%04x", c); }
else esc[w++] = (char)c;
}
esc[w] = '\0';
size_t blen = w + strlen(eg_embed_model()) + 64;
char* body = malloc(blen);
if (!body) { free(esc); return NULL; }
snprintf(body, blen, "{\"model\":\"%s\",\"prompt\":\"%s\"}",
eg_embed_model(), esc);
free(esc);
struct curl_slist* h = curl_slist_append(NULL, "Content-Type: application/json");
el_val_t resp = http_do_t("POST", eg_embed_url(), body, h,
ENGRAM_EMBED_TIMEOUT_MS);
curl_slist_free_all(h);
free(body);
const char* r = EL_CSTR(resp);
const char* arr = r ? strstr(r, "\"embedding\"") : NULL;
if (!arr) {
if (++_eg_embed_consec_fail >= ENGRAM_EMBED_BREAKER_LIMIT) {
_eg_embed_breaker_until = now + ENGRAM_EMBED_BREAKER_COOLDOWN_MS;
_eg_embed_consec_fail = 0;
}
return NULL;
}
arr = strchr(arr, '[');
if (!arr) return NULL;
arr++;
int32_t cap = 1024, dim = 0;
float* v = malloc((size_t)cap * sizeof(float));
if (!v) return NULL;
const char* p = arr;
while (*p && *p != ']') {
char* endp = NULL;
double d = strtod(p, &endp);
if (endp == p) break;
if (dim >= cap) { break; } /* >1024 dims: refuse, model mismatch */
v[dim++] = (float)d;
p = endp;
while (*p == ',' || *p == ' ' || *p == '\n') p++;
}
if (dim < 8) { free(v); return NULL; } /* junk response */
_eg_embed_consec_fail = 0;
*out_dim = dim;
return v;
}
/* Node types that never receive embeddings: pure telemetry and structural
* plumbing. Everything else (Knowledge, Memory, BacklogItem, Entity, ...)
* is eligible. */
static int eg_embed_eligible(const EngramNode* n) {
if (!n->content || strlen(n->content) < 8) return 0;
if (!n->node_type) return 1;
if (strcmp(n->node_type, "InternalStateEvent") == 0) return 0;
if (strcmp(n->node_type, "Tag") == 0) return 0;
return 1;
}
/* Parse a persisted comma-separated float list into node->emb. */
static void eg_parse_emb(EngramNode* nn, const char* s) {
if (!s || !*s) return;
int32_t cap = 1024, dim = 0;
float* v = malloc((size_t)cap * sizeof(float));
if (!v) return;
const char* p = s;
while (*p) {
char* endp = NULL;
double d = strtod(p, &endp);
if (endp == p) break;
if (dim >= cap) break;
v[dim++] = (float)d;
p = endp;
while (*p == ',' || *p == ' ') p++;
}
if (dim < 8) { free(v); return; }
nn->emb = v;
nn->emb_dim = dim;
}
typedef struct EngramEdge {
char* id;
char* from_id;
@@ -6391,6 +6579,9 @@ static el_val_t engram_node_to_map(const EngramNode* n) {
* promotion anchor so heartbeat ISEs / API consumers can see decay state. */
m = el_map_set(m, EL_STR(el_strdup("wm_anchor")), el_from_float(n->wm_anchor));
m = el_map_set(m, EL_STR(el_strdup("base_level")), el_from_float(engram_bll_base_level(n, engram_now_ms())));
/* emb_dim only — the vector itself is too large for map/API output.
* 0 = not yet embedded by the lazy backfill. (2026-07-24) */
m = el_map_set(m, EL_STR(el_strdup("emb_dim")), (el_val_t)(int64_t)n->emb_dim);
return m;
}
@@ -6700,6 +6891,7 @@ void engram_forget(el_val_t node_id) {
EngramNode* n = &g->nodes[idx];
free(n->id); free(n->content); free(n->node_type); free(n->label);
free(n->tier); free(n->tags); free(n->metadata);
free(n->emb);
/* Shift remaining nodes down */
for (int64_t i = idx + 1; i < g->node_count; i++) {
g->nodes[i - 1] = g->nodes[i];
@@ -6785,6 +6977,7 @@ el_val_t engram_prune_telemetry(el_val_t older_than_ms) {
removed_ids[removed++] = n->id; /* keep id for edge sweep */
free(n->content); free(n->node_type); free(n->label);
free(n->tier); free(n->tags); free(n->metadata);
free(n->emb);
} else {
if (w != i) g->nodes[w] = g->nodes[i];
w++;
@@ -7321,12 +7514,68 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
int64_t now_ms = engram_now_ms();
/* ── Embedding backfill + query embedding (2026-07-24, bl-b2d1c944) ──
* Backfill: embed up to N un-embedded eligible nodes per call, newest
* first (append order creation order), so fresh content is semantic-
* searchable within one scan cycle and the historical store fills in
* gradually ~16 nodes/min under the 30s curiosity cadence, no bulk
* hammering of Ollama, no latency on any create path. */
{
int backfilled = 0;
for (int64_t i = g->node_count - 1;
i >= 0 && backfilled < ENGRAM_EMBED_BACKFILL_PER_CALL; i--) {
EngramNode* n = &g->nodes[i];
if (n->emb || !eg_embed_eligible(n)) continue;
int32_t d = 0;
float* v = eg_embed_fetch(n->content, &d);
if (!v) break; /* embedder down / breaker open — stop this call */
n->emb = v; n->emb_dim = d;
backfilled++;
}
}
/* Query embedding, cached single-slot: the curiosity loop re-issues the
* same 4 rotating phrases, so consecutive identical queries skip the
* HTTP round-trip entirely. */
static char* _eg_qcache_text = NULL;
static float* _eg_qcache_emb = NULL;
static int32_t _eg_qcache_dim = 0;
float* q_emb = NULL;
int32_t q_dim = 0;
if (_eg_qcache_text && strcmp(_eg_qcache_text, q) == 0) {
q_emb = _eg_qcache_emb; q_dim = _eg_qcache_dim;
} else {
int32_t d = 0;
float* v = eg_embed_fetch(q, &d);
if (v) {
free(_eg_qcache_text); free(_eg_qcache_emb);
_eg_qcache_text = strdup(q);
_eg_qcache_emb = v;
_eg_qcache_dim = d;
q_emb = v; q_dim = d;
}
}
/* Per-node cosine vs the query, computed once, consumed twice: semantic
* seeding below and the additive WM term in Pass 2 (use similarity
* twice, coherently HippoRAG). cosq stays NULL when the embedder is
* unavailable; every consumer degrades to pure lexical behavior. */
double* cosq = NULL;
if (q_emb) {
cosq = calloc((size_t)g->node_count, sizeof(double));
if (cosq) {
for (int64_t i = 0; i < g->node_count; i++) {
EngramNode* n = &g->nodes[i];
cosq[i] = (n->emb && n->emb_dim == q_dim)
? eg_cosine(n->emb, q_emb, q_dim) : -2.0;
}
}
}
/* Per-node layer-1 tracking. */
double* best_bg = calloc((size_t)g->node_count, sizeof(double));
int64_t* best_hops = calloc((size_t)g->node_count, sizeof(int64_t));
int* reached = calloc((size_t)g->node_count, sizeof(int));
if (!best_bg || !best_hops || !reached) {
free(best_bg); free(best_hops); free(reached); return out;
free(best_bg); free(best_hops); free(reached); free(cosq); return out;
}
/* ── LAYER 1: broad fan-out (background activation) ─────────────────
@@ -7337,7 +7586,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
SeedEntry* seeds = malloc((size_t)g->node_count * sizeof(SeedEntry));
int64_t seed_count = 0;
if (!seeds) {
free(best_bg); free(best_hops); free(reached); return out;
free(best_bg); free(best_hops); free(reached); free(cosq); return out;
}
/* Tokenize once: a node seeds if it matches ANY query token, and its seed
* activation is scaled by token coverage (fraction of distinct query
@@ -7376,6 +7625,35 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
reached[i] = 1;
}
}
/* ── Semantic seed supplement (2026-07-24, bl-b2d1c944) ─────────────
* Top-K nodes by cosine SEED_MIN join the seed set with initial
* activation = similarity × the same decay/dampen shaping the lexical
* seeds get. This is the fix for "idle cognition firing blanks": a
* curiosity phrase like "decision pattern lesson" now ignites nodes
* that MEAN decisions and lessons, not just nodes that contain those
* literal substrings. Lexically-seeded nodes are skipped the lexical
* path already gave them coverage-scaled activation. */
if (cosq) {
for (int k = 0; k < ENGRAM_EMBED_SEED_K; k++) {
int64_t bi = -1; double bc = ENGRAM_EMBED_SEED_MIN;
for (int64_t i = 0; i < g->node_count; i++) {
if (reached[i]) continue;
if (cosq[i] > bc) { bc = cosq[i]; bi = i; }
}
if (bi < 0) break;
EngramNode* n = &g->nodes[bi];
double tdecay = engram_temporal_decay(n, now_ms);
double dampen = engram_activation_dampen(n);
double act = bc * tdecay * dampen;
seeds[seed_count].idx = bi;
seeds[seed_count].act = act;
seeds[seed_count].created_at = n->created_at;
seed_count++;
best_bg[bi] = act;
best_hops[bi] = 0;
reached[bi] = 1;
}
}
/* Compute mean seed created_at for temporal proximity bonus.
* Was a running pairwise average seed_epoch = (seed_epoch + t_s)/2
* which is NOT the arithmetic mean: it exponentially over-weights the
@@ -7394,7 +7672,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
typedef struct { int64_t idx; int64_t hops; double act; } Frontier;
Frontier* fr = malloc((size_t)(g->node_count * (max_depth + 1)) * sizeof(Frontier) + 16 * sizeof(Frontier));
if (!fr) {
free(best_bg); free(best_hops); free(reached); free(seeds); return out;
free(best_bg); free(best_hops); free(reached); free(seeds); free(cosq); return out;
}
int64_t fhead = 0, ftail = 0;
int64_t fcap = (int64_t)((size_t)(g->node_count * (max_depth + 1)) + 16);
@@ -7488,7 +7766,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
double* inhibition = calloc((size_t)g->node_count, sizeof(double));
if (!inhibition) {
free(best_bg); free(best_hops); free(reached); free(seeds); free(fr);
return out;
free(cosq); return out;
}
for (int64_t ei = 0; ei < g->edge_count; ei++) {
EngramEdge* e = &g->edges[ei];
@@ -7513,7 +7791,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
double* wm_weights = calloc((size_t)g->node_count, sizeof(double));
if (!wm_weights) {
free(best_bg); free(best_hops); free(reached); free(seeds);
free(fr); free(inhibition); return out;
free(fr); free(inhibition); free(cosq); return out;
}
for (int64_t i = 0; i < g->node_count; i++) {
if (!reached[i] || best_bg[i] <= 0.0) continue;
@@ -7542,6 +7820,16 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
if (inh > 1.0) inh = 1.0;
double suppress = 1.0 - (1.0 - ENGRAM_INHIBITION_FACTOR) * inh;
raw_wm *= suppress;
/* Additive semantic-relevance term (2026-07-24, bl-b2d1c944):
* shift-and-floor at S0 nomic-embed scores unrelated pairs
* 0.40.5, so raw cosine in a weighted sum would be a constant
* bias swamping the decayed base-level signal. Above S0 the term
* ramps 0 WM_WEIGHT, breaking ties among structurally equivalent
* candidates in favor of nodes that mean what the query means. */
if (cosq && cosq[i] > ENGRAM_EMBED_S0) {
raw_wm += ENGRAM_EMBED_WM_WEIGHT
* (cosq[i] - ENGRAM_EMBED_S0) / (1.0 - ENGRAM_EMBED_S0);
}
/* Threshold gate: must exceed per-type threshold to enter working
* memory. Type threshold replaces the old flat 0.2 filter. */
if (raw_wm >= type_threshold) {
@@ -7760,7 +8048,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
int64_t rcount = 0;
if (!results) {
free(best_bg); free(best_hops); free(reached); free(seeds);
free(fr); free(inhibition); free(wm_weights); return out;
free(fr); free(inhibition); free(wm_weights); free(cosq); return out;
}
for (int64_t i = 0; i < g->node_count; i++) {
if (!reached[i]) continue;
@@ -7805,6 +8093,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
}
free(best_bg); free(best_hops); free(reached);
free(seeds); free(fr); free(inhibition); free(wm_weights); free(results);
free(cosq);
return out;
}
@@ -7849,6 +8138,19 @@ static void engram_emit_node_json(JsonBuf* b, const EngramNode* n) {
}
jb_putc(b, '"');
}
/* Semantic embedding (2026-07-24): compact %.4g comma list — cosine is
* insensitive to 4-sig-fig rounding, and this keeps snapshot bloat to
* ~5KB per embedded node without a base64 codec. Absent field = not
* embedded; the lazy backfill re-embeds eventually if dropped. */
if (n->emb && n->emb_dim > 0) {
jb_puts(b, ",\"emb\":\"");
for (int32_t j = 0; j < n->emb_dim; j++) {
snprintf(tmp, sizeof(tmp), "%s%.4g", j ? "," : "",
(double)n->emb[j]);
jb_puts(b, tmp);
}
jb_putc(b, '"');
}
jb_putc(b, '}');
}
@@ -8094,6 +8396,10 @@ el_val_t engram_load(el_val_t path) {
char* ats = eg_get_str_field(obj, "access_ts");
if (ats) { engram_bll_parse_access(nn, ats); free(ats); }
}
{
char* es = eg_get_str_field(obj, "emb");
if (es) { eg_parse_emb(nn, es); free(es); }
}
int64_t load_idx = g->node_count;
g->node_count++;
if (nn->id && *nn->id) engram_idmap_put(g, nn->id, load_idx);
@@ -8294,6 +8600,10 @@ el_val_t engram_load_merge(el_val_t path) {
char* ats = eg_get_str_field(obj, "access_ts");
if (ats) { engram_bll_parse_access(nn, ats); free(ats); }
}
{
char* es = eg_get_str_field(obj, "emb");
if (es) { eg_parse_emb(nn, es); free(es); }
}
int64_t merge_idx = g->node_count;
g->node_count++;
added_nodes++;
@@ -8738,13 +9048,38 @@ el_val_t engram_wm_top_json(el_val_t n_v) {
el_val_t engram_stats_json(void) {
EngramStore* g = engram_get();
char buf[128];
/* embedded_count: how far the lazy backfill has progressed. The single
* observable that tells the daily self-review whether semantic
* activation is actually accumulating coverage. (2026-07-24) */
int64_t embedded = 0;
for (int64_t i = 0; i < g->node_count; i++) {
if (g->nodes[i].emb) embedded++;
}
char buf[192];
snprintf(buf, sizeof(buf),
"{\"node_count\":%lld,\"edge_count\":%lld,\"layer_count\":%zu}",
(long long)g->node_count, (long long)g->edge_count, g->layer_count);
"{\"node_count\":%lld,\"edge_count\":%lld,\"layer_count\":%zu,"
"\"embedded_count\":%lld}",
(long long)g->node_count, (long long)g->edge_count, g->layer_count,
(long long)embedded);
return el_wrap_str(el_strdup(buf));
}
/* engram_cosine_sim — cosine similarity between two nodes' embeddings.
* Returns a float in [-1, 1], or -2.0 when either node is missing or not
* yet embedded. Exposed so EL code (and the introspection API) can probe
* semantic distance directly. (2026-07-24, bl-b2d1c944) */
el_val_t engram_cosine_sim(el_val_t id_a, el_val_t id_b) {
EngramStore* g = engram_get();
int64_t ia = engram_find_node_index(EL_CSTR(id_a));
int64_t ib = engram_find_node_index(EL_CSTR(id_b));
if (ia < 0 || ib < 0) return el_from_float(-2.0);
EngramNode* a = &g->nodes[ia];
EngramNode* b = &g->nodes[ib];
if (!a->emb || !b->emb || a->emb_dim != b->emb_dim)
return el_from_float(-2.0);
return el_from_float(eg_cosine(a->emb, b->emb, a->emb_dim));
}
/* engram_list_layers_json — serialized counterpart of engram_list_layers.
* Returns a JSON array, sorted by activation_priority ascending. */
el_val_t engram_list_layers_json(void) {
@@ -117,6 +117,15 @@ el_val_t el_min(el_val_t a, el_val_t b);
void el_retain(el_val_t v);
void el_release(el_val_t v);
/* ── Arena scoping ────────────────────────────────────────────────────────────
* el_arena_push() activates the string arena (if not already active) and
* returns a mark; el_arena_pop(mark) frees all strings allocated since that
* mark. Used by codegen for per-function/statement scoping and by long-running
* EL loops (e.g. the soul daemon's awareness tick) to reclaim per-iteration
* allocations. */
el_val_t el_arena_push(void);
el_val_t el_arena_pop(el_val_t mark);
/* ── List ────────────────────────────────────────────────────────────────── */
el_val_t el_list_new(el_val_t count, ...);
@@ -142,6 +151,7 @@ el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map);
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map);
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
el_val_t http_delete(el_val_t url);
el_val_t http_delete_json(el_val_t url, el_val_t json_body);
void http_serve(el_val_t port, el_val_t handler);
void http_set_handler(el_val_t name);
@@ -167,6 +177,11 @@ void http_set_handler(el_val_t name);
void http_serve_v2(el_val_t port, el_val_t handler);
void http_set_handler_v2(el_val_t name);
/* Non-blocking variant of http_serve: runs the accept loop in a background
* pthread and returns immediately so the caller can continue (used by the
* soul daemon to run awareness_run() after starting its HTTP API). */
void http_serve_async(el_val_t port, el_val_t handler);
/* Build an HTTP response envelope. `headers_json` should be a JSON object
* literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The
* returned string carries the discriminator `{"el_http_response":1,...}`
@@ -576,6 +591,7 @@ el_val_t engram_list_layers(void);
el_val_t engram_get_node(el_val_t id);
void engram_strengthen(el_val_t node_id);
void engram_forget(el_val_t node_id);
el_val_t engram_prune_telemetry(el_val_t older_than_ms);
el_val_t engram_node_count(void);
el_val_t engram_search(el_val_t query, el_val_t limit);
el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset);
@@ -594,12 +610,14 @@ el_val_t engram_load(el_val_t path);
* can pass results straight through without round-tripping ElList/ElMap
* through json_stringify. */
el_val_t engram_get_node_json(el_val_t id);
el_val_t engram_get_node_by_label(el_val_t label);
el_val_t engram_search_json(el_val_t query, el_val_t limit);
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
el_val_t engram_stats_json(void);
el_val_t engram_cosine_sim(el_val_t id_a, el_val_t id_b);
el_val_t engram_list_layers_json(void);
/* Working memory introspection — count, mean weight, and top-N snapshot.
* Ported from el-compiler/runtime on 2026-06-30 self-review. */