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:
Vendored
BIN
Binary file not shown.
Vendored
+31
-24
@@ -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
@@ -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 + "\"}"
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user