diff --git a/engram/dist/engram b/engram/dist/engram index 988e3cd..2236bee 100755 Binary files a/engram/dist/engram and b/engram/dist/engram differ diff --git a/engram/dist/engram.c b/engram/dist/engram.c index f43fca5..9c3ffce 100644 --- a/engram/dist/engram.c +++ b/engram/dist/engram.c @@ -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); diff --git a/engram/src/server.el b/engram/src/server.el index 3372ff6..5e72136 100644 --- a/engram/src/server.el +++ b/engram/src/server.el @@ -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 + "\"}" } diff --git a/lang/releases/v1.0.0-20260501/el_runtime.c b/lang/releases/v1.0.0-20260501/el_runtime.c index e9d633c..cf31d3c 100644 --- a/lang/releases/v1.0.0-20260501/el_runtime.c +++ b/lang/releases/v1.0.0-20260501/el_runtime.c @@ -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.4–0.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.4–0.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) { diff --git a/lang/releases/v1.0.0-20260501/el_runtime.h b/lang/releases/v1.0.0-20260501/el_runtime.h index a3dd683..87ac86b 100644 --- a/lang/releases/v1.0.0-20260501/el_runtime.h +++ b/lang/releases/v1.0.0-20260501/el_runtime.h @@ -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. */