self-review 2026-07-18: fix soul SIGABRT double-free + engram route scoping sweep
1. engram_neighbors_json (release runtime): BFS frontier/visited strings were
el_strdup'd (arena-tracked) but manually freed, so el_request_end()
double-freed every one — SIGABRT in http_worker under load (2 prod crashes
today via /api/neuron/session/begin and /api/neuron/graph; reproduced and
verified fixed with ASAN). Introduced when porting from the dev runtime,
which correctly uses plain strdup. Third instance of the
arena-vs-manual-free class (after EngramNode 07-15 and idmap keys 07-16).
2. server.el: let-in-if scoping sweep — defaults assigned inside if-blocks
never mutated the outer binding, so /api/search and /api/activate always
ran with q="", created nodes got node_type=""/salience=0.0, edges got
relation=""/weight=0.0, and save/load with no path hit engram_save("").
Rewritten to the let-if-else expression form. /api/activate now also
rejects empty queries instead of wiping carried WM weights.
3. engram_activate: retrieval reinforcement (ACT-R base-level learning) —
nodes promoted to WM that survive both capacity caps now get
last_activated/activation_count updated, so frequently retrieved memories
decay slower than abandoned ones. Scoped to promoted-only to avoid
flattening dampening across BFS fan-out.
This commit is contained in:
Vendored
+100
-99
@@ -20,16 +20,19 @@ el_val_t route_create_edge(el_val_t method, el_val_t path, el_val_t body);
|
|||||||
el_val_t route_neighbors(el_val_t method, el_val_t path, el_val_t body);
|
el_val_t route_neighbors(el_val_t method, el_val_t path, el_val_t body);
|
||||||
el_val_t route_strengthen(el_val_t method, el_val_t path, el_val_t body);
|
el_val_t route_strengthen(el_val_t method, el_val_t path, el_val_t body);
|
||||||
el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body);
|
el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body);
|
||||||
el_val_t route_create_ise(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 route_save(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 route_load(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 route_health(el_val_t method, el_val_t path, el_val_t body);
|
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 route_emit_ise(el_val_t method, el_val_t path, el_val_t body);
|
||||||
|
el_val_t route_capture_knowledge(el_val_t method, el_val_t path, el_val_t body);
|
||||||
el_val_t check_auth_ok(el_val_t method, el_val_t body);
|
el_val_t check_auth_ok(el_val_t method, el_val_t body);
|
||||||
el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body);
|
el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body);
|
||||||
|
|
||||||
|
el_val_t bind_raw;
|
||||||
el_val_t bind_str;
|
el_val_t bind_str;
|
||||||
el_val_t port;
|
el_val_t port;
|
||||||
|
el_val_t data_dir_raw;
|
||||||
el_val_t data_dir;
|
el_val_t data_dir;
|
||||||
el_val_t snapshot_path;
|
el_val_t snapshot_path;
|
||||||
|
|
||||||
@@ -112,14 +115,10 @@ el_val_t route_stats(el_val_t method, el_val_t path, el_val_t body) {
|
|||||||
|
|
||||||
el_val_t route_create_node(el_val_t method, el_val_t path, el_val_t body) {
|
el_val_t route_create_node(el_val_t method, el_val_t path, el_val_t body) {
|
||||||
el_val_t content = json_get_string(body, EL_STR("content"));
|
el_val_t content = json_get_string(body, EL_STR("content"));
|
||||||
el_val_t node_type = json_get_string(body, EL_STR("node_type"));
|
el_val_t nt_raw = json_get_string(body, EL_STR("node_type"));
|
||||||
if (str_eq(node_type, EL_STR(""))) {
|
el_val_t node_type = ({ el_val_t _if_result_1 = 0; if (str_eq(nt_raw, EL_STR(""))) { _if_result_1 = (EL_STR("Memory")); } else { _if_result_1 = (nt_raw); } _if_result_1; });
|
||||||
node_type = EL_STR("Memory");
|
el_val_t sal_raw = json_get_float(body, EL_STR("salience"));
|
||||||
}
|
el_val_t salience = ({ el_val_t _if_result_2 = 0; if ((sal_raw == el_from_float(0.0))) { _if_result_2 = (el_from_float(0.5)); } else { _if_result_2 = (sal_raw); } _if_result_2; });
|
||||||
el_val_t salience = json_get_float(body, EL_STR("salience"));
|
|
||||||
if (salience == el_from_float(0.0)) {
|
|
||||||
salience = el_from_float(0.5);
|
|
||||||
}
|
|
||||||
el_val_t id = engram_node(content, node_type, salience);
|
el_val_t id = engram_node(content, node_type, salience);
|
||||||
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 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;
|
return 0;
|
||||||
@@ -146,10 +145,8 @@ 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 route_scan_edges(el_val_t method, el_val_t path, el_val_t body) {
|
||||||
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
|
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||||
if (str_eq(dir, EL_STR(""))) {
|
el_val_t dir = ({ el_val_t _if_result_3 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_3 = (EL_STR("/tmp/engram")); } else { _if_result_3 = (dir_raw); } _if_result_3; });
|
||||||
dir = EL_STR("/tmp/engram");
|
|
||||||
}
|
|
||||||
el_val_t snap_path = el_str_concat(dir, EL_STR("/snapshot.json"));
|
el_val_t snap_path = el_str_concat(dir, EL_STR("/snapshot.json"));
|
||||||
engram_save(snap_path);
|
engram_save(snap_path);
|
||||||
el_val_t snap = fs_read(snap_path);
|
el_val_t snap = fs_read(snap_path);
|
||||||
@@ -165,36 +162,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 route_search(el_val_t method, el_val_t path, el_val_t body) {
|
||||||
el_val_t q = EL_STR("");
|
el_val_t q = ({ el_val_t _if_result_4 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_4 = (query_param(path, EL_STR("q"))); } else { _if_result_4 = (json_get_string(body, EL_STR("query"))); } _if_result_4; });
|
||||||
if (str_eq(method, EL_STR("GET"))) {
|
el_val_t lim_url = query_int(path, EL_STR("limit"), 0);
|
||||||
q = query_param(path, EL_STR("q"));
|
el_val_t lim_body = json_get_int(body, EL_STR("limit"));
|
||||||
} else {
|
el_val_t lim_either = ({ el_val_t _if_result_5 = 0; if ((lim_url > 0)) { _if_result_5 = (lim_url); } else { _if_result_5 = (lim_body); } _if_result_5; });
|
||||||
q = json_get_string(body, EL_STR("query"));
|
el_val_t limit = ({ el_val_t _if_result_6 = 0; if ((lim_either > 0)) { _if_result_6 = (lim_either); } else { _if_result_6 = (20); } _if_result_6; });
|
||||||
}
|
|
||||||
el_val_t limit = query_int(path, EL_STR("limit"), 20);
|
|
||||||
if (limit == 0) {
|
|
||||||
limit = json_get_int(body, EL_STR("limit"));
|
|
||||||
}
|
|
||||||
if (limit == 0) {
|
|
||||||
limit = 20;
|
|
||||||
}
|
|
||||||
return engram_search_json(q, limit);
|
return engram_search_json(q, limit);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
el_val_t route_activate(el_val_t method, el_val_t path, el_val_t body) {
|
el_val_t route_activate(el_val_t method, el_val_t path, el_val_t body) {
|
||||||
el_val_t q = EL_STR("");
|
el_val_t q = ({ el_val_t _if_result_7 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_7 = (query_param(path, EL_STR("q"))); } else { _if_result_7 = (json_get_string(body, EL_STR("query"))); } _if_result_7; });
|
||||||
el_val_t depth = 3;
|
if (str_eq(q, EL_STR(""))) {
|
||||||
if (str_eq(method, EL_STR("GET"))) {
|
return err_json(EL_STR("missing query"));
|
||||||
q = query_param(path, EL_STR("q"));
|
|
||||||
depth = query_int(path, EL_STR("depth"), 3);
|
|
||||||
} else {
|
|
||||||
q = json_get_string(body, EL_STR("query"));
|
|
||||||
el_val_t bd = json_get_int(body, EL_STR("depth"));
|
|
||||||
if (bd > 0) {
|
|
||||||
depth = bd;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
el_val_t d_raw = ({ el_val_t _if_result_8 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_8 = (query_int(path, EL_STR("depth"), 3)); } else { _if_result_8 = (json_get_int(body, EL_STR("depth"))); } _if_result_8; });
|
||||||
|
el_val_t depth = ({ el_val_t _if_result_9 = 0; if ((d_raw > 0)) { _if_result_9 = (d_raw); } else { _if_result_9 = (3); } _if_result_9; });
|
||||||
return el_str_concat(el_str_concat(EL_STR("{\"results\":"), engram_activate_json(q, depth)), EL_STR("}"));
|
return el_str_concat(el_str_concat(EL_STR("{\"results\":"), engram_activate_json(q, depth)), EL_STR("}"));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -202,14 +185,10 @@ el_val_t route_activate(el_val_t method, el_val_t path, el_val_t body) {
|
|||||||
el_val_t route_create_edge(el_val_t method, el_val_t path, el_val_t body) {
|
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 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 to_id = json_get_string(body, EL_STR("to_id"));
|
||||||
el_val_t relation = json_get_string(body, EL_STR("relation"));
|
el_val_t rel_raw = json_get_string(body, EL_STR("relation"));
|
||||||
if (str_eq(relation, EL_STR(""))) {
|
el_val_t relation = ({ el_val_t _if_result_10 = 0; if (str_eq(rel_raw, EL_STR(""))) { _if_result_10 = (EL_STR("associates")); } else { _if_result_10 = (rel_raw); } _if_result_10; });
|
||||||
relation = EL_STR("associates");
|
el_val_t w_raw = json_get_float(body, EL_STR("weight"));
|
||||||
}
|
el_val_t weight = ({ el_val_t _if_result_11 = 0; if ((w_raw == el_from_float(0.0))) { _if_result_11 = (el_from_float(0.5)); } else { _if_result_11 = (w_raw); } _if_result_11; });
|
||||||
el_val_t weight = json_get_float(body, EL_STR("weight"));
|
|
||||||
if (weight == el_from_float(0.0)) {
|
|
||||||
weight = el_from_float(0.5);
|
|
||||||
}
|
|
||||||
engram_connect(from_id, to_id, weight, relation);
|
engram_connect(from_id, to_id, weight, relation);
|
||||||
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("\"}"));
|
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("\"}"));
|
||||||
return 0;
|
return 0;
|
||||||
@@ -245,25 +224,35 @@ el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
el_val_t route_create_ise(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 content = json_get_string(body, EL_STR("content"));
|
el_val_t p_raw = json_get_string(body, EL_STR("path"));
|
||||||
if (str_eq(content, EL_STR(""))) {
|
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||||
return err_json(EL_STR("missing content"));
|
el_val_t dir = ({ el_val_t _if_result_12 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_12 = (EL_STR("/tmp/engram")); } else { _if_result_12 = (dir_raw); } _if_result_12; });
|
||||||
}
|
el_val_t p = ({ el_val_t _if_result_13 = 0; if (str_eq(p_raw, EL_STR(""))) { _if_result_13 = (el_str_concat(dir, EL_STR("/snapshot.json"))); } else { _if_result_13 = (p_raw); } _if_result_13; });
|
||||||
el_val_t sal = el_from_float(0.3);
|
engram_save(p);
|
||||||
el_val_t imp = el_from_float(0.3);
|
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"path\":\""), p), EL_STR("\"}"));
|
||||||
el_val_t conf = el_from_float(0.8);
|
return 0;
|
||||||
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\"]"));
|
}
|
||||||
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), id), EL_STR("\"}"));
|
|
||||||
|
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_14 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_14 = (EL_STR("/tmp/engram")); } else { _if_result_14 = (dir_raw); } _if_result_14; });
|
||||||
|
el_val_t p = ({ el_val_t _if_result_15 = 0; if (str_eq(p_raw, EL_STR(""))) { _if_result_15 = (el_str_concat(dir, EL_STR("/snapshot.json"))); } else { _if_result_15 = (p_raw); } _if_result_15; });
|
||||||
|
engram_load(p);
|
||||||
|
return ok_json();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
el_val_t route_health(el_val_t method, el_val_t path, el_val_t body) {
|
||||||
|
return EL_STR("{\"status\":\"ok\",\"engine\":\"engram-runtime-native\"}");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
el_val_t route_sync(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 = env(EL_STR("ENGRAM_DATA_DIR"));
|
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||||
if (str_eq(dir, EL_STR(""))) {
|
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; });
|
||||||
dir = EL_STR("/tmp/engram");
|
el_val_t snap_path = el_str_concat(dir, EL_STR("/snapshot.json"));
|
||||||
}
|
|
||||||
el_val_t snap_path = el_str_concat(dir, EL_STR("/sync-export.json"));
|
|
||||||
engram_save(snap_path);
|
engram_save(snap_path);
|
||||||
el_val_t snap = fs_read(snap_path);
|
el_val_t snap = fs_read(snap_path);
|
||||||
if (str_eq(snap, EL_STR(""))) {
|
if (str_eq(snap, EL_STR(""))) {
|
||||||
@@ -273,36 +262,49 @@ el_val_t route_sync(el_val_t method, el_val_t path, el_val_t body) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
el_val_t route_save(el_val_t method, el_val_t path, el_val_t body) {
|
el_val_t route_emit_ise(el_val_t method, el_val_t path, el_val_t body) {
|
||||||
el_val_t p = json_get_string(body, EL_STR("path"));
|
el_val_t content = json_get_string(body, EL_STR("content"));
|
||||||
if (str_eq(p, EL_STR(""))) {
|
if (str_eq(content, EL_STR(""))) {
|
||||||
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
|
return err_json(EL_STR("missing content"));
|
||||||
if (str_eq(dir, EL_STR(""))) {
|
|
||||||
dir = EL_STR("/tmp/engram");
|
|
||||||
}
|
|
||||||
p = el_str_concat(dir, EL_STR("/snapshot.json"));
|
|
||||||
}
|
}
|
||||||
engram_save(p);
|
el_val_t sal = el_from_float(0.3);
|
||||||
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"path\":\""), p), EL_STR("\"}"));
|
el_val_t imp = el_from_float(0.3);
|
||||||
|
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_17 = 0; if (str_eq(ret_raw, EL_STR(""))) { _if_result_17 = (172800000); } else { _if_result_17 = (str_to_int(ret_raw)); } _if_result_17; });
|
||||||
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
el_val_t route_load(el_val_t method, el_val_t path, el_val_t body) {
|
el_val_t route_capture_knowledge(el_val_t method, el_val_t path, el_val_t body) {
|
||||||
el_val_t p = json_get_string(body, EL_STR("path"));
|
el_val_t content = json_get_string(body, EL_STR("content"));
|
||||||
if (str_eq(p, EL_STR(""))) {
|
if (str_eq(content, EL_STR(""))) {
|
||||||
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
|
return err_json(EL_STR("missing content"));
|
||||||
if (str_eq(dir, EL_STR(""))) {
|
|
||||||
dir = EL_STR("/tmp/engram");
|
|
||||||
}
|
|
||||||
p = el_str_concat(dir, EL_STR("/snapshot.json"));
|
|
||||||
}
|
}
|
||||||
engram_load(p);
|
el_val_t title = json_get_string(body, EL_STR("title"));
|
||||||
return ok_json();
|
el_val_t label = ({ el_val_t _if_result_18 = 0; if (str_eq(title, EL_STR(""))) { _if_result_18 = (str_slice(content, 0, 60)); } else { _if_result_18 = (title); } _if_result_18; });
|
||||||
return 0;
|
el_val_t category_raw = json_get_string(body, EL_STR("category"));
|
||||||
}
|
el_val_t category = ({ el_val_t _if_result_19 = 0; if (str_eq(category_raw, EL_STR(""))) { _if_result_19 = (EL_STR("other")); } else { _if_result_19 = (category_raw); } _if_result_19; });
|
||||||
|
el_val_t ktier_raw = json_get_string(body, EL_STR("tier"));
|
||||||
el_val_t route_health(el_val_t method, el_val_t path, el_val_t body) {
|
el_val_t ktier = ({ el_val_t _if_result_20 = 0; if (str_eq(ktier_raw, EL_STR(""))) { _if_result_20 = (EL_STR("note")); } else { _if_result_20 = (ktier_raw); } _if_result_20; });
|
||||||
return EL_STR("{\"status\":\"ok\",\"engine\":\"engram-runtime-native\"}");
|
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_21 = 0; if (str_eq(tags_raw, EL_STR(""))) { _if_result_21 = (EL_STR("[]")); } else { _if_result_21 = (tags_raw); } _if_result_21; });
|
||||||
|
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_22 = 0; if (str_eq(head, EL_STR("["))) { _if_result_22 = (EL_STR("")); } else { _if_result_22 = (EL_STR(",")); } _if_result_22; });
|
||||||
|
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_23 = 0; if (str_eq(safe_proj, EL_STR(""))) { _if_result_23 = (EL_STR("")); } else { _if_result_23 = (el_str_concat(el_str_concat(EL_STR(",\"project:"), safe_proj), EL_STR("\""))); } _if_result_23; });
|
||||||
|
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);
|
||||||
|
el_val_t conf = el_from_float(0.9);
|
||||||
|
el_val_t id = engram_node_full(content, EL_STR("Knowledge"), label, sal, imp, conf, EL_STR("Semantic"), tags);
|
||||||
|
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), id), EL_STR("\"}"));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,12 +331,15 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
|
|||||||
return route_health(method, path, body);
|
return route_health(method, path, body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (str_eq(method, EL_STR("POST")) && str_starts_with(clean, EL_STR("/api/neuron/state-events"))) {
|
if (str_eq(method, EL_STR("POST")) && str_eq(clean, EL_STR("/api/neuron/state-events"))) {
|
||||||
return route_create_ise(method, path, body);
|
return route_emit_ise(method, path, body);
|
||||||
}
|
}
|
||||||
if (!check_auth_ok(method, body)) {
|
if (!check_auth_ok(method, body)) {
|
||||||
return err_json(EL_STR("unauthorized"));
|
return err_json(EL_STR("unauthorized"));
|
||||||
}
|
}
|
||||||
|
if (str_eq(method, EL_STR("POST")) && str_eq(clean, EL_STR("/api/neuron/knowledge/capture"))) {
|
||||||
|
return route_capture_knowledge(method, path, body);
|
||||||
|
}
|
||||||
if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/stats")) || str_eq(clean, EL_STR("/stats")))) {
|
if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/stats")) || str_eq(clean, EL_STR("/stats")))) {
|
||||||
return route_stats(method, path, body);
|
return route_stats(method, path, body);
|
||||||
}
|
}
|
||||||
@@ -374,30 +379,26 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
|
|||||||
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/strengthen")) || str_eq(clean, EL_STR("/strengthen")))) {
|
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/strengthen")) || str_eq(clean, EL_STR("/strengthen")))) {
|
||||||
return route_strengthen(method, path, body);
|
return route_strengthen(method, path, body);
|
||||||
}
|
}
|
||||||
if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/sync")) || str_eq(clean, EL_STR("/sync")))) {
|
|
||||||
return route_sync(method, path, body);
|
|
||||||
}
|
|
||||||
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/save")) || str_eq(clean, EL_STR("/save")))) {
|
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/save")) || str_eq(clean, EL_STR("/save")))) {
|
||||||
return route_save(method, path, body);
|
return route_save(method, path, body);
|
||||||
}
|
}
|
||||||
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/load")) || str_eq(clean, EL_STR("/load")))) {
|
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/load")) || str_eq(clean, EL_STR("/load")))) {
|
||||||
return route_load(method, path, body);
|
return route_load(method, path, body);
|
||||||
}
|
}
|
||||||
|
if (str_eq(method, EL_STR("GET")) && str_eq(clean, EL_STR("/api/sync"))) {
|
||||||
|
return route_sync(method, path, body);
|
||||||
|
}
|
||||||
return el_str_concat(el_str_concat(EL_STR("{\"error\":\"not found\",\"path\":\""), clean), EL_STR("\"}"));
|
return el_str_concat(el_str_concat(EL_STR("{\"error\":\"not found\",\"path\":\""), clean), EL_STR("\"}"));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int _argc, char** _argv) {
|
int main(int _argc, char** _argv) {
|
||||||
el_runtime_init_args(_argc, _argv);
|
el_runtime_init_args(_argc, _argv);
|
||||||
bind_str = env(EL_STR("ENGRAM_BIND"));
|
bind_raw = env(EL_STR("ENGRAM_BIND"));
|
||||||
if (str_eq(bind_str, EL_STR(""))) {
|
bind_str = ({ el_val_t _if_result_24 = 0; if (str_eq(bind_raw, EL_STR(""))) { _if_result_24 = (EL_STR(":8742")); } else { _if_result_24 = (bind_raw); } _if_result_24; });
|
||||||
bind_str = EL_STR(":8742");
|
|
||||||
}
|
|
||||||
port = parse_port(bind_str);
|
port = parse_port(bind_str);
|
||||||
data_dir = env(EL_STR("ENGRAM_DATA_DIR"));
|
data_dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||||
if (str_eq(data_dir, EL_STR(""))) {
|
data_dir = ({ el_val_t _if_result_25 = 0; if (str_eq(data_dir_raw, EL_STR(""))) { _if_result_25 = (EL_STR("/tmp/engram")); } else { _if_result_25 = (data_dir_raw); } _if_result_25; });
|
||||||
data_dir = EL_STR("/tmp/engram");
|
|
||||||
}
|
|
||||||
snapshot_path = el_str_concat(data_dir, EL_STR("/snapshot.json"));
|
snapshot_path = el_str_concat(data_dir, EL_STR("/snapshot.json"));
|
||||||
engram_load(snapshot_path);
|
engram_load(snapshot_path);
|
||||||
println(EL_STR("[engram] runtime-native graph engine"));
|
println(EL_STR("[engram] runtime-native graph engine"));
|
||||||
|
|||||||
+120
-49
@@ -76,12 +76,21 @@ fn route_stats(method: String, path: String, body: String) -> String {
|
|||||||
engram_stats_json()
|
engram_stats_json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// (2026-07-18 self-review) Scoping sweep: `let` inside an if-block creates an
|
||||||
|
// inner scope only — it does NOT mutate the outer binding (documented with
|
||||||
|
// evidence in awareness.el, 2026-05-25). Every default/reassignment below used
|
||||||
|
// that broken pattern, so defaults never applied: nodes were created with
|
||||||
|
// node_type="" and salience=0.0, /api/search and /api/activate ALWAYS ran with
|
||||||
|
// q="" regardless of input, edges defaulted to relation=""/weight=0.0, and
|
||||||
|
// save/load with no "path" hit engram_save(""). Rewritten to the
|
||||||
|
// `let x = if cond { a } else { b }` expression form (the pattern the newer
|
||||||
|
// routes route_emit_ise/route_capture_knowledge already use correctly).
|
||||||
fn route_create_node(method: String, path: String, body: String) -> String {
|
fn route_create_node(method: String, path: String, body: String) -> String {
|
||||||
let content: String = json_get_string(body, "content")
|
let content: String = json_get_string(body, "content")
|
||||||
let node_type: String = json_get_string(body, "node_type")
|
let nt_raw: String = json_get_string(body, "node_type")
|
||||||
if str_eq(node_type, "") { let node_type = "Memory" }
|
let node_type: String = if str_eq(nt_raw, "") { "Memory" } else { nt_raw }
|
||||||
let salience: Float = json_get_float(body, "salience")
|
let sal_raw: Float = json_get_float(body, "salience")
|
||||||
if salience == 0.0 { let salience = 0.5 }
|
let salience: Float = if sal_raw == 0.0 { 0.5 } else { sal_raw }
|
||||||
let id: String = engram_node(content, node_type, salience)
|
let id: String = engram_node(content, node_type, salience)
|
||||||
"{\"id\":\"" + id + "\",\"content\":\"" + content + "\",\"node_type\":\"" + node_type + "\"}"
|
"{\"id\":\"" + id + "\",\"content\":\"" + content + "\",\"node_type\":\"" + node_type + "\"}"
|
||||||
}
|
}
|
||||||
@@ -107,8 +116,8 @@ fn route_scan_nodes(method: String, path: String, body: String) -> String {
|
|||||||
// runtime keeps in lockstep with the in-memory graph. Live against the
|
// runtime keeps in lockstep with the in-memory graph. Live against the
|
||||||
// running graph, not a stale export.
|
// running graph, not a stale export.
|
||||||
fn route_scan_edges(method: String, path: String, body: String) -> String {
|
fn route_scan_edges(method: String, path: String, body: String) -> String {
|
||||||
let dir: String = env("ENGRAM_DATA_DIR")
|
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||||
if str_eq(dir, "") { let dir = "/tmp/engram" }
|
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||||||
let snap_path: String = dir + "/snapshot.json"
|
let snap_path: String = dir + "/snapshot.json"
|
||||||
engram_save(snap_path)
|
engram_save(snap_path)
|
||||||
let snap: String = fs_read(snap_path)
|
let snap: String = fs_read(snap_path)
|
||||||
@@ -122,39 +131,32 @@ fn route_scan_edges(method: String, path: String, body: String) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn route_search(method: String, path: String, body: String) -> String {
|
fn route_search(method: String, path: String, body: String) -> String {
|
||||||
let q: String = ""
|
let q: String = if str_eq(method, "GET") { query_param(path, "q") } else { json_get_string(body, "query") }
|
||||||
if str_eq(method, "GET") {
|
let lim_url: Int = query_int(path, "limit", 0)
|
||||||
let q = query_param(path, "q")
|
let lim_body: Int = json_get_int(body, "limit")
|
||||||
} else {
|
let lim_either: Int = if lim_url > 0 { lim_url } else { lim_body }
|
||||||
let q = json_get_string(body, "query")
|
let limit: Int = if lim_either > 0 { lim_either } else { 20 }
|
||||||
}
|
|
||||||
let limit: Int = query_int(path, "limit", 20)
|
|
||||||
if limit == 0 { let limit = json_get_int(body, "limit") }
|
|
||||||
if limit == 0 { let limit = 20 }
|
|
||||||
return engram_search_json(q, limit)
|
return engram_search_json(q, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn route_activate(method: String, path: String, body: String) -> String {
|
fn route_activate(method: String, path: String, body: String) -> String {
|
||||||
let q: String = ""
|
let q: String = if str_eq(method, "GET") { query_param(path, "q") } else { json_get_string(body, "query") }
|
||||||
let depth: Int = 3
|
// Guard: engram_activate with an empty query matches zero seeds, which
|
||||||
if str_eq(method, "GET") {
|
// zeroes ALL carried working-memory weights (documented in awareness.el
|
||||||
let q = query_param(path, "q")
|
// perceive()). Never let an empty activation through to wipe WM.
|
||||||
let depth = query_int(path, "depth", 3)
|
if str_eq(q, "") { return err_json("missing query") }
|
||||||
} else {
|
let d_raw: Int = if str_eq(method, "GET") { query_int(path, "depth", 3) } else { json_get_int(body, "depth") }
|
||||||
let q = json_get_string(body, "query")
|
let depth: Int = if d_raw > 0 { d_raw } else { 3 }
|
||||||
let bd: Int = json_get_int(body, "depth")
|
|
||||||
if bd > 0 { let depth = bd }
|
|
||||||
}
|
|
||||||
return "{\"results\":" + engram_activate_json(q, depth) + "}"
|
return "{\"results\":" + engram_activate_json(q, depth) + "}"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn route_create_edge(method: String, path: String, body: String) -> String {
|
fn route_create_edge(method: String, path: String, body: String) -> String {
|
||||||
let from_id: String = json_get_string(body, "from_id")
|
let from_id: String = json_get_string(body, "from_id")
|
||||||
let to_id: String = json_get_string(body, "to_id")
|
let to_id: String = json_get_string(body, "to_id")
|
||||||
let relation: String = json_get_string(body, "relation")
|
let rel_raw: String = json_get_string(body, "relation")
|
||||||
if str_eq(relation, "") { let relation = "associates" }
|
let relation: String = if str_eq(rel_raw, "") { "associates" } else { rel_raw }
|
||||||
let weight: Float = json_get_float(body, "weight")
|
let w_raw: Float = json_get_float(body, "weight")
|
||||||
if weight == 0.0 { let weight = 0.5 }
|
let weight: Float = if w_raw == 0.0 { 0.5 } else { w_raw }
|
||||||
engram_connect(from_id, to_id, weight, relation)
|
engram_connect(from_id, to_id, weight, relation)
|
||||||
"{\"ok\":true,\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + relation + "\"}"
|
"{\"ok\":true,\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + relation + "\"}"
|
||||||
}
|
}
|
||||||
@@ -181,23 +183,19 @@ fn route_forget(method: String, path: String, body: String) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn route_save(method: String, path: String, body: String) -> String {
|
fn route_save(method: String, path: String, body: String) -> String {
|
||||||
let p: String = json_get_string(body, "path")
|
let p_raw: String = json_get_string(body, "path")
|
||||||
if str_eq(p, "") {
|
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||||
let dir: String = env("ENGRAM_DATA_DIR")
|
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||||||
if str_eq(dir, "") { let dir = "/tmp/engram" }
|
let p: String = if str_eq(p_raw, "") { dir + "/snapshot.json" } else { p_raw }
|
||||||
let p = dir + "/snapshot.json"
|
|
||||||
}
|
|
||||||
engram_save(p)
|
engram_save(p)
|
||||||
"{\"ok\":true,\"path\":\"" + p + "\"}"
|
"{\"ok\":true,\"path\":\"" + p + "\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn route_load(method: String, path: String, body: String) -> String {
|
fn route_load(method: String, path: String, body: String) -> String {
|
||||||
let p: String = json_get_string(body, "path")
|
let p_raw: String = json_get_string(body, "path")
|
||||||
if str_eq(p, "") {
|
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||||
let dir: String = env("ENGRAM_DATA_DIR")
|
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||||||
if str_eq(dir, "") { let dir = "/tmp/engram" }
|
let p: String = if str_eq(p_raw, "") { dir + "/snapshot.json" } else { p_raw }
|
||||||
let p = dir + "/snapshot.json"
|
|
||||||
}
|
|
||||||
engram_load(p)
|
engram_load(p)
|
||||||
ok_json()
|
ok_json()
|
||||||
}
|
}
|
||||||
@@ -219,8 +217,8 @@ fn route_health(method: String, path: String, body: String) -> String {
|
|||||||
// (it skips nodes already present by ID). Auth-exempt: same-host internal call.
|
// (it skips nodes already present by ID). Auth-exempt: same-host internal call.
|
||||||
// (2026-06-27 self-review: added this route to fix silent 10-min sync failures)
|
// (2026-06-27 self-review: added this route to fix silent 10-min sync failures)
|
||||||
fn route_sync(method: String, path: String, body: String) -> String {
|
fn route_sync(method: String, path: String, body: String) -> String {
|
||||||
let dir: String = env("ENGRAM_DATA_DIR")
|
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||||
if str_eq(dir, "") { let dir = "/tmp/engram" }
|
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||||||
let snap_path: String = dir + "/snapshot.json"
|
let snap_path: String = dir + "/snapshot.json"
|
||||||
engram_save(snap_path)
|
engram_save(snap_path)
|
||||||
let snap: String = fs_read(snap_path)
|
let snap: String = fs_read(snap_path)
|
||||||
@@ -241,10 +239,20 @@ fn route_sync(method: String, path: String, body: String) -> String {
|
|||||||
//
|
//
|
||||||
// Salience/importance set to match engram_node_full ISE defaults used by the
|
// Salience/importance set to match engram_node_full ISE defaults used by the
|
||||||
// in-process fallback path in awareness.el (salience=0.3, importance=0.3,
|
// in-process fallback path in awareness.el (salience=0.3, importance=0.3,
|
||||||
// confidence=0.8, tier=Episodic). High temporal_decay_rate (1.617) — ISEs
|
// confidence=0.8, tier=Episodic).
|
||||||
// are inherently transient; they should decay faster than structural knowledge.
|
|
||||||
// (2026-06-26 self-review: added this route after discovering ise_post was
|
// (2026-06-26 self-review: added this route after discovering ise_post was
|
||||||
// silently failing — the soul posts here but the endpoint didn't exist.)
|
// silently failing — the soul posts here but the endpoint didn't exist.)
|
||||||
|
//
|
||||||
|
// Retention (2026-07-16 self-review): an earlier comment here claimed ISEs
|
||||||
|
// got temporal_decay_rate=1.617 — that was never implemented (engram_node_full
|
||||||
|
// hardcodes 0.0), and per-node decay only dampens activation anyway; it never
|
||||||
|
// removes nodes. By 2026-07-16 ISEs were 75% of the store (10,175 of 13,522
|
||||||
|
// nodes, ~4,300/day, unbounded). ISEs are already WM-excluded in
|
||||||
|
// engram_activate, so the fix is retention, not decay: every insert calls
|
||||||
|
// engram_prune_telemetry(), a single O(nodes+edges) compaction pass that
|
||||||
|
// removes ISEs older than ENGRAM_ISE_RETENTION_MS (default 48h), protecting
|
||||||
|
// "session-start" labels and self_review events as durable history. At
|
||||||
|
// ~3 ISEs/min this bounds telemetry at ~8.6k nodes instead of growing forever.
|
||||||
fn route_emit_ise(method: String, path: String, body: String) -> String {
|
fn route_emit_ise(method: String, path: String, body: String) -> String {
|
||||||
let content: String = json_get_string(body, "content")
|
let content: String = json_get_string(body, "content")
|
||||||
if str_eq(content, "") { return err_json("missing content") }
|
if str_eq(content, "") { return err_json("missing content") }
|
||||||
@@ -256,6 +264,63 @@ fn route_emit_ise(method: String, path: String, body: String) -> String {
|
|||||||
sal, imp, conf,
|
sal, imp, conf,
|
||||||
"Episodic", "[\"internal-state\",\"InternalStateEvent\"]"
|
"Episodic", "[\"internal-state\",\"InternalStateEvent\"]"
|
||||||
)
|
)
|
||||||
|
let ret_raw: String = env("ENGRAM_ISE_RETENTION_MS")
|
||||||
|
let ret_ms: Int = if str_eq(ret_raw, "") { 172800000 } else { str_to_int(ret_raw) }
|
||||||
|
let pruned: Int = engram_prune_telemetry(ret_ms)
|
||||||
|
"{\"ok\":true,\"id\":\"" + id + "\",\"pruned\":" + int_to_str(pruned) + "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Knowledge capture ─────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// route_capture_knowledge — direct Knowledge-node capture over HTTP.
|
||||||
|
//
|
||||||
|
// Endpoint: POST /api/neuron/knowledge/capture (auth required: "_auth" in body)
|
||||||
|
// Body: {"content": "...", "title": "...", "category": "...",
|
||||||
|
// "tier": "note|lesson|canonical", "tags": [...], "project": "...",
|
||||||
|
// "_auth": "<key>"}
|
||||||
|
//
|
||||||
|
// WHY (2026-07-15 self-review): the world-ingestor integrator was designed
|
||||||
|
// against this endpoint (its MCP-unavailable fallback), but the route never
|
||||||
|
// existed — every direct push 404'd, and because the auth gate ran before
|
||||||
|
// routing, the failure surfaced as {"error":"unauthorized"} and was
|
||||||
|
// misdiagnosed for two weeks while world knowledge silently dropped.
|
||||||
|
// POST /api/nodes was no substitute: it discards label/tags/tier, which
|
||||||
|
// makes captured knowledge invisible to tag-scoped search and curiosity.
|
||||||
|
//
|
||||||
|
// The incoming knowledge tier (note/lesson/canonical) is preserved as a
|
||||||
|
// "tier:<x>" tag rather than mapped onto Engram's cognitive tiers — Knowledge
|
||||||
|
// nodes land in Semantic (stable reference), and the epistemic tier stays
|
||||||
|
// queryable without inventing a lossy mapping.
|
||||||
|
fn route_capture_knowledge(method: String, path: String, body: String) -> String {
|
||||||
|
let content: String = json_get_string(body, "content")
|
||||||
|
if str_eq(content, "") { return err_json("missing content") }
|
||||||
|
let title: String = json_get_string(body, "title")
|
||||||
|
let label: String = if str_eq(title, "") { str_slice(content, 0, 60) } else { title }
|
||||||
|
let category_raw: String = json_get_string(body, "category")
|
||||||
|
let category: String = if str_eq(category_raw, "") { "other" } else { category_raw }
|
||||||
|
let ktier_raw: String = json_get_string(body, "tier")
|
||||||
|
let ktier: String = if str_eq(ktier_raw, "") { "note" } else { ktier_raw }
|
||||||
|
let project: String = json_get_string(body, "project")
|
||||||
|
let tags_raw: String = json_get_raw(body, "tags")
|
||||||
|
let tags_base: String = if str_eq(tags_raw, "") { "[]" } else { tags_raw }
|
||||||
|
// Merge category/tier/project markers into the tag array. Search matches
|
||||||
|
// against the tags string, so these make captures findable by facet.
|
||||||
|
let base_len: Int = str_len(tags_base)
|
||||||
|
let head: String = str_slice(tags_base, 0, base_len - 1)
|
||||||
|
let sep: String = if str_eq(head, "[") { "" } else { "," }
|
||||||
|
let safe_cat: String = str_replace(category, "\"", "'")
|
||||||
|
let safe_tier: String = str_replace(ktier, "\"", "'")
|
||||||
|
let safe_proj: String = str_replace(project, "\"", "'")
|
||||||
|
let proj_tag: String = if str_eq(safe_proj, "") { "" } else { ",\"project:" + safe_proj + "\"" }
|
||||||
|
let tags: String = head + sep + "\"category:" + safe_cat + "\",\"tier:" + safe_tier + "\"" + proj_tag + "]"
|
||||||
|
let sal: Float = 0.5
|
||||||
|
let imp: Float = 0.5
|
||||||
|
let conf: Float = 0.9
|
||||||
|
let id: String = engram_node_full(
|
||||||
|
content, "Knowledge", label,
|
||||||
|
sal, imp, conf,
|
||||||
|
"Semantic", tags
|
||||||
|
)
|
||||||
"{\"ok\":true,\"id\":\"" + id + "\"}"
|
"{\"ok\":true,\"id\":\"" + id + "\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,6 +360,12 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
|||||||
return err_json("unauthorized")
|
return err_json("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Knowledge capture (auth enforced above; the world-ingestor integrator
|
||||||
|
// and any headless session without MCP push knowledge through this)
|
||||||
|
if str_eq(method, "POST") && str_eq(clean, "/api/neuron/knowledge/capture") {
|
||||||
|
return route_capture_knowledge(method, path, body)
|
||||||
|
}
|
||||||
|
|
||||||
// Stats
|
// Stats
|
||||||
if str_eq(method, "GET") && (str_eq(clean, "/api/stats") || str_eq(clean, "/stats")) {
|
if str_eq(method, "GET") && (str_eq(clean, "/api/stats") || str_eq(clean, "/stats")) {
|
||||||
return route_stats(method, path, body)
|
return route_stats(method, path, body)
|
||||||
@@ -362,13 +433,13 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
|||||||
|
|
||||||
// ── Entry ─────────────────────────────────────────────────────────────────────
|
// ── Entry ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
let bind_str: String = env("ENGRAM_BIND")
|
let bind_raw: String = env("ENGRAM_BIND")
|
||||||
if str_eq(bind_str, "") { let bind_str = ":8742" }
|
let bind_str: String = if str_eq(bind_raw, "") { ":8742" } else { bind_raw }
|
||||||
let port: Int = parse_port(bind_str)
|
let port: Int = parse_port(bind_str)
|
||||||
|
|
||||||
// On startup, try to load any existing snapshot (best effort).
|
// On startup, try to load any existing snapshot (best effort).
|
||||||
let data_dir: String = env("ENGRAM_DATA_DIR")
|
let data_dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||||
if str_eq(data_dir, "") { let data_dir = "/tmp/engram" }
|
let data_dir: String = if str_eq(data_dir_raw, "") { "/tmp/engram" } else { data_dir_raw }
|
||||||
let snapshot_path: String = data_dir + "/snapshot.json"
|
let snapshot_path: String = data_dir + "/snapshot.json"
|
||||||
engram_load(snapshot_path)
|
engram_load(snapshot_path)
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,47 @@ void el_request_end(void) {
|
|||||||
_tl_arena.count = 0;
|
_tl_arena.count = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Scoped arena for CLI use ─────────────────────────────────────────────── *
|
||||||
|
* CLI programs never call el_request_start/end, so all strdup allocations are
|
||||||
|
* permanent. el_arena_push/pop let the compiler free intermediate strings
|
||||||
|
* after each compilation unit. Ported verbatim from el-compiler/runtime on
|
||||||
|
* 2026-07-17: the soul daemon's awareness loop arena-scopes each tick with
|
||||||
|
* these, and they were present only in the dev runtime copy.
|
||||||
|
*
|
||||||
|
* el_arena_push() — activates the arena if not already active, saves the
|
||||||
|
* current arena count as a mark, and returns it as an el_val_t Int.
|
||||||
|
* el_arena_pop(mark) — frees all strings allocated since the push mark and
|
||||||
|
* resets the count. If count reaches 0, deactivates the arena.
|
||||||
|
*/
|
||||||
|
#define EL_ARENA_SCOPE_DEPTH 32
|
||||||
|
static _Thread_local size_t _tl_arena_scope[EL_ARENA_SCOPE_DEPTH];
|
||||||
|
static _Thread_local int _tl_arena_scope_depth = 0;
|
||||||
|
|
||||||
|
el_val_t el_arena_push(void) {
|
||||||
|
if (!_tl_arena_active) {
|
||||||
|
_tl_arena_active = 1;
|
||||||
|
}
|
||||||
|
if (_tl_arena_scope_depth < EL_ARENA_SCOPE_DEPTH) {
|
||||||
|
_tl_arena_scope[_tl_arena_scope_depth++] = _tl_arena.count;
|
||||||
|
}
|
||||||
|
return (el_val_t)(int64_t)_tl_arena.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
el_val_t el_arena_pop(el_val_t mark) {
|
||||||
|
size_t save = (size_t)(int64_t)mark;
|
||||||
|
if (save > _tl_arena.count) save = 0;
|
||||||
|
for (size_t i = save; i < _tl_arena.count; i++) {
|
||||||
|
if (_tl_arena.ptrs[i]) {
|
||||||
|
free(_tl_arena.ptrs[i]);
|
||||||
|
_tl_arena.ptrs[i] = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_tl_arena.count = save;
|
||||||
|
if (_tl_arena_scope_depth > 0) _tl_arena_scope_depth--;
|
||||||
|
if (save == 0) _tl_arena_active = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Persistent allocation — bypasses the arena (state_set, engram internals). */
|
/* Persistent allocation — bypasses the arena (state_set, engram internals). */
|
||||||
static char* el_strdup_persist(const char* s) {
|
static char* el_strdup_persist(const char* s) {
|
||||||
if (!s) return strdup("");
|
if (!s) return strdup("");
|
||||||
@@ -1518,6 +1559,86 @@ void http_serve(el_val_t port, el_val_t handler) {
|
|||||||
close(sock);
|
close(sock);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── http_serve_async — non-blocking HTTP server ─────────────────────────── */
|
||||||
|
/* Runs the accept loop in a background pthread, returns immediately so the
|
||||||
|
* calling EL script can continue (e.g. to run an awareness loop).
|
||||||
|
* Ported verbatim from el-compiler/runtime on 2026-07-17: the soul daemon
|
||||||
|
* (soul.el) builds against this release runtime and calls http_serve_async,
|
||||||
|
* which was present only in the dev runtime copy.
|
||||||
|
*
|
||||||
|
* El signature: http_serve_async(port, handler) -> Void */
|
||||||
|
|
||||||
|
typedef struct { int sock; } HttpServeAsyncArg;
|
||||||
|
|
||||||
|
static void* _http_serve_async_loop(void* raw) {
|
||||||
|
HttpServeAsyncArg* a = (HttpServeAsyncArg*)raw;
|
||||||
|
int sock = a->sock;
|
||||||
|
free(a);
|
||||||
|
while (1) {
|
||||||
|
struct sockaddr_in6 cli;
|
||||||
|
socklen_t clen = sizeof(cli);
|
||||||
|
int cfd = accept(sock, (struct sockaddr*)&cli, &clen);
|
||||||
|
if (cfd < 0) {
|
||||||
|
if (errno == EINTR) continue;
|
||||||
|
perror("accept"); break;
|
||||||
|
}
|
||||||
|
pthread_mutex_lock(&_http_conn_mu);
|
||||||
|
while (_http_conn_active >= HTTP_MAX_CONNS) {
|
||||||
|
pthread_cond_wait(&_http_conn_cv, &_http_conn_mu);
|
||||||
|
}
|
||||||
|
_http_conn_active++;
|
||||||
|
pthread_mutex_unlock(&_http_conn_mu);
|
||||||
|
HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg));
|
||||||
|
if (!arg) { close(cfd); continue; }
|
||||||
|
arg->fd = cfd;
|
||||||
|
pthread_t tid;
|
||||||
|
if (pthread_create(&tid, NULL, http_worker, arg) != 0) {
|
||||||
|
close(cfd); free(arg);
|
||||||
|
pthread_mutex_lock(&_http_conn_mu);
|
||||||
|
_http_conn_active--;
|
||||||
|
pthread_cond_signal(&_http_conn_cv);
|
||||||
|
pthread_mutex_unlock(&_http_conn_mu);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pthread_detach(tid);
|
||||||
|
}
|
||||||
|
close(sock);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void http_serve_async(el_val_t port, el_val_t handler) {
|
||||||
|
const char* hname = EL_CSTR(handler);
|
||||||
|
if (hname && looks_like_string(handler)) {
|
||||||
|
http_set_handler(handler);
|
||||||
|
}
|
||||||
|
int p = (int)port;
|
||||||
|
if (p <= 0 || p > 65535) { fprintf(stderr, "http_serve_async: invalid port %d\n", p); return; }
|
||||||
|
int sock = socket(AF_INET6, SOCK_STREAM, 0);
|
||||||
|
if (sock < 0) { perror("socket"); return; }
|
||||||
|
int yes = 1; int no = 0;
|
||||||
|
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
||||||
|
setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no));
|
||||||
|
struct sockaddr_in6 addr;
|
||||||
|
memset(&addr, 0, sizeof(addr));
|
||||||
|
addr.sin6_family = AF_INET6;
|
||||||
|
addr.sin6_addr = in6addr_any;
|
||||||
|
addr.sin6_port = htons((uint16_t)p);
|
||||||
|
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||||
|
perror("bind"); close(sock); return;
|
||||||
|
}
|
||||||
|
if (listen(sock, 64) < 0) { perror("listen"); close(sock); return; }
|
||||||
|
fprintf(stderr, "[http] async listening on [::]:%d (dual-stack)\n", p);
|
||||||
|
HttpServeAsyncArg* a = malloc(sizeof(HttpServeAsyncArg));
|
||||||
|
if (!a) { close(sock); return; }
|
||||||
|
a->sock = sock;
|
||||||
|
pthread_t tid;
|
||||||
|
if (pthread_create(&tid, NULL, _http_serve_async_loop, a) != 0) {
|
||||||
|
perror("pthread_create"); free(a); close(sock); return;
|
||||||
|
}
|
||||||
|
pthread_detach(tid);
|
||||||
|
/* Returns immediately — caller can now run awareness_run() or any loop. */
|
||||||
|
}
|
||||||
|
|
||||||
/* ── HTTP server v2 — request headers + structured response ──────────────── */
|
/* ── HTTP server v2 — request headers + structured response ──────────────── */
|
||||||
/*
|
/*
|
||||||
* v2 widens the handler signature from
|
* v2 widens the handler signature from
|
||||||
@@ -5926,7 +6047,16 @@ static void engram_idmap_put(EngramStore* g, const char* id, int64_t idx) {
|
|||||||
}
|
}
|
||||||
/* Use tombstone slot if found (avoids growing used count unnecessarily) */
|
/* Use tombstone slot if found (avoids growing used count unnecessarily) */
|
||||||
if (tomb_slot != SIZE_MAX) slot = tomb_slot;
|
if (tomb_slot != SIZE_MAX) slot = tomb_slot;
|
||||||
g->id_map[slot].key = el_strdup(id);
|
/* MUST be el_strdup_persist: idmap keys outlive the request/tick arena.
|
||||||
|
* (2026-07-16 self-review) This was el_strdup (arena-tracked): every node
|
||||||
|
* created inside an HTTP request left its idmap key DANGLING as soon as
|
||||||
|
* el_request_end() freed the arena — subsequent lookups strcmp'd freed
|
||||||
|
* memory, and any idmap_free/rebuild in a later request double-freed it
|
||||||
|
* (SIGABRT in http_worker; found via ASAN when engram_prune_telemetry
|
||||||
|
* triggered an in-request rebuild). Same allocation-discipline class as
|
||||||
|
* the 2026-07-15 EngramNode fix — see the store-persistent comment above
|
||||||
|
* engram_new_id(). */
|
||||||
|
g->id_map[slot].key = el_strdup_persist(id);
|
||||||
g->id_map[slot].idx = idx;
|
g->id_map[slot].idx = idx;
|
||||||
g->id_map_used++;
|
g->id_map_used++;
|
||||||
}
|
}
|
||||||
@@ -6102,10 +6232,23 @@ static void engram_grow_edges(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Build a fresh UUID string. Reuses uuid_new but takes the underlying char*. */
|
/* Build a fresh UUID string. Reuses uuid_new but takes the underlying char*. */
|
||||||
|
/* ── Store-persistent allocation discipline ─────────────────────────────────
|
||||||
|
* (2026-07-15 self-review) EngramNode/EngramEdge string fields OUTLIVE the
|
||||||
|
* request/tick arena they were created in. The 2026-07-13 leak-fix made
|
||||||
|
* el_strdup arena-tracked, which silently turned every node created inside
|
||||||
|
* an HTTP request (route_emit_ise, route_create_node, knowledge capture) or
|
||||||
|
* inside the soul's per-tick arena (engram_load_merge in the refresh cycle)
|
||||||
|
* into a bag of dangling pointers the moment the arena popped: readback by
|
||||||
|
* id returned {}, type-filtered scans skipped them, search returned request
|
||||||
|
* memory reused as node content, and snapshots persisted garbage ("numeric
|
||||||
|
* tier strings"). Everything written into the store must go through
|
||||||
|
* el_strdup_persist / el_strbuf_persist (plain malloc — free() in
|
||||||
|
* engram_forget/evolve remains valid). Arena-tracked el_strdup remains
|
||||||
|
* correct for RETURN values handed back to EL code. */
|
||||||
static char* engram_new_id(void) {
|
static char* engram_new_id(void) {
|
||||||
el_val_t v = uuid_new();
|
el_val_t v = uuid_new();
|
||||||
const char* s = EL_CSTR(v);
|
const char* s = EL_CSTR(v);
|
||||||
return el_strdup(s ? s : "");
|
return el_strdup_persist(s ? s : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Convert a node into an ElMap of its fields. */
|
/* Convert a node into an ElMap of its fields. */
|
||||||
@@ -6166,12 +6309,12 @@ el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience) {
|
|||||||
n->id = engram_new_id();
|
n->id = engram_new_id();
|
||||||
const char* c = EL_CSTR(content);
|
const char* c = EL_CSTR(content);
|
||||||
const char* nt = EL_CSTR(node_type);
|
const char* nt = EL_CSTR(node_type);
|
||||||
n->content = el_strdup(c ? c : "");
|
n->content = el_strdup_persist(c ? c : "");
|
||||||
n->node_type = el_strdup(nt && *nt ? nt : "Memory");
|
n->node_type = el_strdup_persist(nt && *nt ? nt : "Memory");
|
||||||
n->label = engram_first_n_chars(c, 60);
|
n->label = el_strdup_persist(engram_first_n_chars(c, 60));
|
||||||
n->tier = el_strdup("Working");
|
n->tier = el_strdup_persist("Working");
|
||||||
n->tags = el_strdup("");
|
n->tags = el_strdup_persist("");
|
||||||
n->metadata = el_strdup("{}");
|
n->metadata = el_strdup_persist("{}");
|
||||||
n->salience = engram_decode_score(salience);
|
n->salience = engram_decode_score(salience);
|
||||||
if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5;
|
if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5;
|
||||||
n->importance = 0.5;
|
n->importance = 0.5;
|
||||||
@@ -6203,12 +6346,12 @@ el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label,
|
|||||||
const char* lb = EL_CSTR(label);
|
const char* lb = EL_CSTR(label);
|
||||||
const char* ti = EL_CSTR(tier);
|
const char* ti = EL_CSTR(tier);
|
||||||
const char* tg = EL_CSTR(tags);
|
const char* tg = EL_CSTR(tags);
|
||||||
n->content = el_strdup(c ? c : "");
|
n->content = el_strdup_persist(c ? c : "");
|
||||||
n->node_type = el_strdup(nt && *nt ? nt : "Memory");
|
n->node_type = el_strdup_persist(nt && *nt ? nt : "Memory");
|
||||||
n->label = el_strdup(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : ""));
|
n->label = el_strdup_persist(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : ""));
|
||||||
n->tier = el_strdup(ti && *ti ? ti : "Working");
|
n->tier = el_strdup_persist(ti && *ti ? ti : "Working");
|
||||||
n->tags = el_strdup(tg ? tg : "");
|
n->tags = el_strdup_persist(tg ? tg : "");
|
||||||
n->metadata = el_strdup("{}");
|
n->metadata = el_strdup_persist("{}");
|
||||||
n->salience = engram_decode_score(salience);
|
n->salience = engram_decode_score(salience);
|
||||||
n->importance = engram_decode_score(importance);
|
n->importance = engram_decode_score(importance);
|
||||||
n->confidence = engram_decode_score(confidence);
|
n->confidence = engram_decode_score(confidence);
|
||||||
@@ -6259,20 +6402,20 @@ el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t labe
|
|||||||
const char* lb = EL_CSTR(label);
|
const char* lb = EL_CSTR(label);
|
||||||
const char* tg = EL_CSTR(tags);
|
const char* tg = EL_CSTR(tags);
|
||||||
const char* st = EL_CSTR(status);
|
const char* st = EL_CSTR(status);
|
||||||
n->content = el_strdup(c ? c : "");
|
n->content = el_strdup_persist(c ? c : "");
|
||||||
n->node_type = el_strdup(nt && *nt ? nt : "Memory");
|
n->node_type = el_strdup_persist(nt && *nt ? nt : "Memory");
|
||||||
n->label = el_strdup(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : ""));
|
n->label = el_strdup_persist(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : ""));
|
||||||
n->tier = el_strdup("Working");
|
n->tier = el_strdup_persist("Working");
|
||||||
n->tags = el_strdup(tg ? tg : "");
|
n->tags = el_strdup_persist(tg ? tg : "");
|
||||||
if (st && *st) {
|
if (st && *st) {
|
||||||
/* Minimal metadata payload: {"status":"..."}. Keep it cheap so
|
/* Minimal metadata payload: {"status":"..."}. Keep it cheap so
|
||||||
* callers using `status` don't pay JSON parse cost on every read. */
|
* callers using `status` don't pay JSON parse cost on every read. */
|
||||||
size_t sl = strlen(st) + 16;
|
size_t sl = strlen(st) + 16;
|
||||||
char* meta = el_strbuf(sl);
|
char* meta = el_strbuf_persist(sl);
|
||||||
snprintf(meta, sl, "{\"status\":\"%s\"}", st);
|
snprintf(meta, sl, "{\"status\":\"%s\"}", st);
|
||||||
n->metadata = meta;
|
n->metadata = meta;
|
||||||
} else {
|
} else {
|
||||||
n->metadata = el_strdup("{}");
|
n->metadata = el_strdup_persist("{}");
|
||||||
}
|
}
|
||||||
n->salience = engram_decode_score(salience);
|
n->salience = engram_decode_score(salience);
|
||||||
n->importance = engram_decode_score(certainty);
|
n->importance = engram_decode_score(certainty);
|
||||||
@@ -6468,6 +6611,114 @@ el_val_t engram_node_count(void) {
|
|||||||
return (el_val_t)engram_get()->node_count;
|
return (el_val_t)engram_get()->node_count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Telemetry retention ────────────────────────────────────────────────────
|
||||||
|
* (2026-07-16 self-review) InternalStateEvent nodes are append-only telemetry
|
||||||
|
* (heartbeat, curiosity_scan, engram_sync) written ~3/min by the awareness
|
||||||
|
* loop. Nothing ever removed them: by July 16 they were 10,175 of 13,522
|
||||||
|
* nodes — 75% of the store was telemetry. They are already force-excluded
|
||||||
|
* from WM promotion (engram_activate), so their only effect was store bloat,
|
||||||
|
* snapshot bloat, and lexical-search noise.
|
||||||
|
*
|
||||||
|
* engram_prune_telemetry(older_than_ms) batch-removes ISE nodes whose
|
||||||
|
* created_at is older than now - older_than_ms, EXCEPT durable markers:
|
||||||
|
* - label "session-start" (boot history)
|
||||||
|
* - content containing "self_review" (daily review trail)
|
||||||
|
* Unlike repeated engram_forget (O(n) shift each), this is a single
|
||||||
|
* compaction pass over nodes plus one pass over edges, with one idmap
|
||||||
|
* rebuild — O(nodes + edges) total, safe to call on every ISE insert.
|
||||||
|
* Returns the number of nodes removed. */
|
||||||
|
|
||||||
|
/* FNV-1a hash for the removed-id set used by the edge sweep. */
|
||||||
|
static uint64_t eg_fnv1a(const char* s) {
|
||||||
|
uint64_t h = 1469598103934665603ULL;
|
||||||
|
while (*s) { h ^= (unsigned char)*s++; h *= 1099511628211ULL; }
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
el_val_t engram_prune_telemetry(el_val_t older_than_ms) {
|
||||||
|
int64_t horizon = (int64_t)older_than_ms;
|
||||||
|
if (horizon <= 0) horizon = 172800000; /* default 48h */
|
||||||
|
EngramStore* g = engram_get();
|
||||||
|
int64_t cutoff = engram_now_ms() - horizon;
|
||||||
|
|
||||||
|
/* Pass 1: mark. Collect ids of prunable nodes (ownership transferred —
|
||||||
|
* strings freed after the edge sweep). */
|
||||||
|
int64_t cap = 0;
|
||||||
|
for (int64_t i = 0; i < g->node_count; i++) {
|
||||||
|
EngramNode* n = &g->nodes[i];
|
||||||
|
if (n->node_type && strcmp(n->node_type, "InternalStateEvent") == 0 &&
|
||||||
|
n->created_at < cutoff) cap++;
|
||||||
|
}
|
||||||
|
if (cap == 0) return 0;
|
||||||
|
|
||||||
|
char** removed_ids = malloc((size_t)cap * sizeof(char*));
|
||||||
|
if (!removed_ids) return 0;
|
||||||
|
int64_t removed = 0, w = 0;
|
||||||
|
for (int64_t i = 0; i < g->node_count; i++) {
|
||||||
|
EngramNode* n = &g->nodes[i];
|
||||||
|
int prunable =
|
||||||
|
n->node_type && strcmp(n->node_type, "InternalStateEvent") == 0 &&
|
||||||
|
n->created_at < cutoff &&
|
||||||
|
!(n->label && strcmp(n->label, "session-start") == 0) &&
|
||||||
|
!(n->content && strstr(n->content, "self_review"));
|
||||||
|
if (prunable && removed < cap) {
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
if (w != i) g->nodes[w] = g->nodes[i];
|
||||||
|
w++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g->node_count = w;
|
||||||
|
if (removed == 0) { free(removed_ids); return 0; }
|
||||||
|
|
||||||
|
/* Removed-id hash set (open addressing, power-of-two >= 2*removed). */
|
||||||
|
size_t set_cap = 16;
|
||||||
|
while (set_cap < (size_t)removed * 2) set_cap <<= 1;
|
||||||
|
const char** set = calloc(set_cap, sizeof(char*));
|
||||||
|
if (set) {
|
||||||
|
for (int64_t i = 0; i < removed; i++) {
|
||||||
|
size_t slot = eg_fnv1a(removed_ids[i]) & (set_cap - 1);
|
||||||
|
while (set[slot]) slot = (slot + 1) & (set_cap - 1);
|
||||||
|
set[slot] = removed_ids[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Pass 2: drop edges incident to any removed node (defensive — ISEs
|
||||||
|
* currently have no edges, but callers may connect them later). */
|
||||||
|
if (set) {
|
||||||
|
int64_t ew = 0;
|
||||||
|
for (int64_t r = 0; r < g->edge_count; r++) {
|
||||||
|
EngramEdge* e = &g->edges[r];
|
||||||
|
int incident = 0;
|
||||||
|
const char* ends[2] = { e->from_id, e->to_id };
|
||||||
|
for (int k = 0; k < 2 && !incident; k++) {
|
||||||
|
if (!ends[k]) continue;
|
||||||
|
size_t slot = eg_fnv1a(ends[k]) & (set_cap - 1);
|
||||||
|
while (set[slot]) {
|
||||||
|
if (strcmp(set[slot], ends[k]) == 0) { incident = 1; break; }
|
||||||
|
slot = (slot + 1) & (set_cap - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (incident) {
|
||||||
|
free(e->id); free(e->from_id); free(e->to_id);
|
||||||
|
free(e->relation); free(e->metadata);
|
||||||
|
} else {
|
||||||
|
if (ew != r) g->edges[ew] = g->edges[r];
|
||||||
|
ew++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g->edge_count = ew;
|
||||||
|
free(set);
|
||||||
|
}
|
||||||
|
for (int64_t i = 0; i < removed; i++) free(removed_ids[i]);
|
||||||
|
free(removed_ids);
|
||||||
|
|
||||||
|
engram_idmap_rebuild(g);
|
||||||
|
engram_adj_free(g);
|
||||||
|
return (el_val_t)removed;
|
||||||
|
}
|
||||||
|
|
||||||
static int istr_contains(const char* hay, const char* needle) {
|
static int istr_contains(const char* hay, const char* needle) {
|
||||||
if (!hay || !needle || !*needle) return 0;
|
if (!hay || !needle || !*needle) return 0;
|
||||||
size_t nl = strlen(needle);
|
size_t nl = strlen(needle);
|
||||||
@@ -6552,10 +6803,10 @@ void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t
|
|||||||
EngramEdge* e = &g->edges[g->edge_count];
|
EngramEdge* e = &g->edges[g->edge_count];
|
||||||
memset(e, 0, sizeof(*e));
|
memset(e, 0, sizeof(*e));
|
||||||
e->id = engram_new_id();
|
e->id = engram_new_id();
|
||||||
e->from_id = el_strdup(f);
|
e->from_id = el_strdup_persist(f);
|
||||||
e->to_id = el_strdup(t);
|
e->to_id = el_strdup_persist(t);
|
||||||
e->relation = el_strdup(r && *r ? r : "associate");
|
e->relation = el_strdup_persist(r && *r ? r : "associate");
|
||||||
e->metadata = el_strdup("{}");
|
e->metadata = el_strdup_persist("{}");
|
||||||
e->weight = engram_decode_score(weight);
|
e->weight = engram_decode_score(weight);
|
||||||
if (e->weight <= 0.0 || e->weight > 1.0) e->weight = 0.5;
|
if (e->weight <= 0.0 || e->weight > 1.0) e->weight = 0.5;
|
||||||
e->confidence = 1.0;
|
e->confidence = 1.0;
|
||||||
@@ -6853,6 +7104,14 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
|||||||
}
|
}
|
||||||
for (int64_t i = 0; i < g->node_count; i++) {
|
for (int64_t i = 0; i < g->node_count; i++) {
|
||||||
EngramNode* n = &g->nodes[i];
|
EngramNode* n = &g->nodes[i];
|
||||||
|
/* InternalStateEvent nodes are observability-only telemetry — never
|
||||||
|
* seed activation from them. Their JSON payloads contain common words
|
||||||
|
* ("memory", "context", ...) that lexically match almost any query,
|
||||||
|
* turning telemetry into a spreading-activation ignition source. They
|
||||||
|
* are already excluded from WM promotion in pass 2; exclude them from
|
||||||
|
* seeding here too. */
|
||||||
|
if (n->node_type && strcmp(n->node_type, "InternalStateEvent") == 0)
|
||||||
|
continue;
|
||||||
if (istr_contains(n->content, q) ||
|
if (istr_contains(n->content, q) ||
|
||||||
istr_contains(n->label, q) ||
|
istr_contains(n->label, q) ||
|
||||||
istr_contains(n->tags, q)) {
|
istr_contains(n->tags, q)) {
|
||||||
@@ -6929,11 +7188,22 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
|||||||
if (oi < 0 || oi >= g->node_count) continue;
|
if (oi < 0 || oi >= g->node_count) continue;
|
||||||
EngramEdge* e = &g->edges[ei];
|
EngramEdge* e = &g->edges[ei];
|
||||||
EngramNode* on = &g->nodes[oi];
|
EngramNode* on = &g->nodes[oi];
|
||||||
|
/* Never propagate INTO InternalStateEvent nodes. They are already
|
||||||
|
* barred from WM promotion (pass 2) and from seeding (above), but
|
||||||
|
* as high-degree hubs they still relayed activation across the
|
||||||
|
* graph. Skipping here keeps them out of the frontier entirely. */
|
||||||
|
if (on->node_type && strcmp(on->node_type, "InternalStateEvent") == 0)
|
||||||
|
continue;
|
||||||
double tbonus = engram_temporal_proximity_bonus(on->created_at, seed_epoch);
|
double tbonus = engram_temporal_proximity_bonus(on->created_at, seed_epoch);
|
||||||
double tdecay = engram_temporal_decay(on, now_ms);
|
double tdecay = engram_temporal_decay(on, now_ms);
|
||||||
double dampen = engram_activation_dampen(on);
|
double dampen = engram_activation_dampen(on);
|
||||||
double new_act = f.act * e->weight * SPREAD_DECAY * (1.0 + tbonus)
|
double new_act = f.act * e->weight * SPREAD_DECAY * (1.0 + tbonus)
|
||||||
* tdecay * dampen;
|
* tdecay * dampen;
|
||||||
|
/* Firing threshold per classic spreading-activation: sub-threshold
|
||||||
|
* activation neither updates the target nor enqueues it, so weak
|
||||||
|
* signals die out instead of flooding the whole graph with tiny
|
||||||
|
* nonzero background activation. */
|
||||||
|
if (new_act < 0.02) continue;
|
||||||
if (!reached[oi] || new_act > best_bg[oi]) {
|
if (!reached[oi] || new_act > best_bg[oi]) {
|
||||||
best_bg[oi] = new_act;
|
best_bg[oi] = new_act;
|
||||||
best_hops[oi] = new_hops;
|
best_hops[oi] = new_hops;
|
||||||
@@ -7174,6 +7444,31 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Retrieval reinforcement (2026-07-18 self-review) ───────────────────
|
||||||
|
* ACT-R base-level learning: retrieval strengthens memory. Before this,
|
||||||
|
* NOTHING in engram_activate updated last_activated/activation_count —
|
||||||
|
* only the rarely-called engram_strengthen did. Consequence: temporal
|
||||||
|
* decay aged every node from its last explicit strengthen (usually
|
||||||
|
* creation), so frequently-retrieved memories decayed identically to
|
||||||
|
* abandoned ones, and engram_activation_dampen() saw a frozen count.
|
||||||
|
*
|
||||||
|
* Scope deliberately narrow: reinforce ONLY nodes promoted to WM in THIS
|
||||||
|
* call that survived both capacity caps (wm_weights[i] > 0 = promoted this
|
||||||
|
* call; working_memory_weight > 0 = survived Pass 4/5 eviction). BFS
|
||||||
|
* fan-out touches thousands of nodes per curiosity scan — reinforcing all
|
||||||
|
* of them would flatten dampening and freeze decay globally. Promotion to
|
||||||
|
* working memory is the analog of actual retrieval (executive access),
|
||||||
|
* matching ACT-R where only completed retrievals add a base-level
|
||||||
|
* presentation. Carry-over nodes (reached[i]==0) are NOT reinforced: they
|
||||||
|
* persist by decay, they were not re-retrieved. */
|
||||||
|
for (int64_t i = 0; i < g->node_count; i++) {
|
||||||
|
if (!reached[i] || wm_weights[i] <= 0.0) continue;
|
||||||
|
EngramNode* n = &g->nodes[i];
|
||||||
|
if (n->working_memory_weight <= 0.0) continue; /* evicted by cap */
|
||||||
|
n->last_activated = now_ms;
|
||||||
|
n->activation_count++;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Collect all background-activated nodes for the return value ────
|
/* ── Collect all background-activated nodes for the return value ────
|
||||||
* Callers see both layers. Context compilation uses only promoted nodes
|
* Callers see both layers. Context compilation uses only promoted nodes
|
||||||
* (working_memory_weight > 0). Sort: promoted first by wm_weight desc,
|
* (working_memory_weight > 0). Sort: promoted first by wm_weight desc,
|
||||||
@@ -7331,12 +7626,21 @@ el_val_t engram_save(el_val_t path) {
|
|||||||
|
|
||||||
/* Helper: extract a string field from a JSON object substring. */
|
/* Helper: extract a string field from a JSON object substring. */
|
||||||
static char* eg_get_str_field(const char* obj, const char* key) {
|
static char* eg_get_str_field(const char* obj, const char* key) {
|
||||||
|
/* Returns a STORE-PERSISTENT string: callers assign the result directly
|
||||||
|
* into EngramNode/EngramEdge fields, and engram_load_merge runs inside
|
||||||
|
* the soul's per-tick arena. jp_parse_string_raw's success buffer is
|
||||||
|
* plain malloc (persist-safe, returned as-is); the empty/error returns
|
||||||
|
* were arena-tracked el_strdup("") — those dangled after the tick arena
|
||||||
|
* popped and free()ing them in callers was a latent double-free. */
|
||||||
const char* p = json_find_key(obj, key);
|
const char* p = json_find_key(obj, key);
|
||||||
if (!p) return el_strdup("");
|
if (!p) return el_strdup_persist("");
|
||||||
if (*p != '"') return el_strdup("");
|
if (*p != '"') return el_strdup_persist("");
|
||||||
JsonParser jp = { .p = p, .end = p + strlen(p), .err = 0 };
|
JsonParser jp = { .p = p, .end = p + strlen(p), .err = 0 };
|
||||||
char* out = jp_parse_string_raw(&jp);
|
char* out = jp_parse_string_raw(&jp);
|
||||||
if (jp.err) { free(out); return el_strdup(""); }
|
/* *p == '"' is guaranteed above, so jp_parse_string_raw took its malloc
|
||||||
|
* path (its arena-tracked early-return only fires on a non-'"' start) —
|
||||||
|
* free(out) on error is safe here. */
|
||||||
|
if (jp.err) { free(out); return el_strdup_persist(""); }
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7358,6 +7662,48 @@ static const char* eg_skip_ws(const char* p) {
|
|||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* eg_enforce_wm_cap_on_load — clamp a snapshot-restored working-memory
|
||||||
|
* population to ENGRAM_WM_CAP.
|
||||||
|
*
|
||||||
|
* WHY (2026-07-15 self-review): engram_load restored working_memory_weight
|
||||||
|
* verbatim from the snapshot with no cap. Pass 4/5 cap enforcement only runs
|
||||||
|
* inside engram_activate — so a snapshot frozen in the pre-2026-06-30 era
|
||||||
|
* (87-111 promoted nodes, all at the old 0.25 breakthrough floor) reloaded
|
||||||
|
* as-is on every boot, and heartbeats faithfully reported the stale
|
||||||
|
* population (wm_active 87-111, wm_avg pinned at exactly 0.25) forever.
|
||||||
|
* The cap must hold at every entry point that materializes WM, not just
|
||||||
|
* the activation path. Same top-K-by-weight logic as engram_activate
|
||||||
|
* Pass 5 (Cowan 2001: WM capacity is global). */
|
||||||
|
static void eg_enforce_wm_cap_on_load(EngramStore* g) {
|
||||||
|
int64_t wm_count = 0;
|
||||||
|
for (int64_t i = 0; i < g->node_count; i++) {
|
||||||
|
if (g->nodes[i].working_memory_weight > 0.0) wm_count++;
|
||||||
|
}
|
||||||
|
if (wm_count <= ENGRAM_WM_CAP) return;
|
||||||
|
double* vals = malloc((size_t)wm_count * sizeof(double));
|
||||||
|
if (!vals) return; /* skip on OOM — over cap this boot, no corruption */
|
||||||
|
int64_t vi = 0;
|
||||||
|
for (int64_t i = 0; i < g->node_count; i++) {
|
||||||
|
if (g->nodes[i].working_memory_weight > 0.0)
|
||||||
|
vals[vi++] = g->nodes[i].working_memory_weight;
|
||||||
|
}
|
||||||
|
qsort(vals, (size_t)wm_count, sizeof(double), engram_cmp_double_desc);
|
||||||
|
double cutoff = vals[ENGRAM_WM_CAP - 1];
|
||||||
|
free(vals);
|
||||||
|
int64_t above = 0;
|
||||||
|
for (int64_t i = 0; i < g->node_count; i++) {
|
||||||
|
if (g->nodes[i].working_memory_weight > cutoff) above++;
|
||||||
|
}
|
||||||
|
int64_t slots_at_cutoff = ENGRAM_WM_CAP - above;
|
||||||
|
for (int64_t i = 0; i < g->node_count; i++) {
|
||||||
|
EngramNode* n = &g->nodes[i];
|
||||||
|
if (n->working_memory_weight <= 0.0) continue;
|
||||||
|
if (n->working_memory_weight > cutoff) continue;
|
||||||
|
if (slots_at_cutoff > 0) { slots_at_cutoff--; continue; }
|
||||||
|
n->working_memory_weight = 0.0; /* evict: over cap at load */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
el_val_t engram_load(el_val_t path) {
|
el_val_t engram_load(el_val_t path) {
|
||||||
const char* p = EL_CSTR(path);
|
const char* p = EL_CSTR(path);
|
||||||
if (!p || !*p) return 0;
|
if (!p || !*p) return 0;
|
||||||
@@ -7412,7 +7758,7 @@ el_val_t engram_load(el_val_t path) {
|
|||||||
nn->tier = eg_get_str_field(obj, "tier");
|
nn->tier = eg_get_str_field(obj, "tier");
|
||||||
nn->tags = eg_get_str_field(obj, "tags");
|
nn->tags = eg_get_str_field(obj, "tags");
|
||||||
nn->metadata = eg_get_str_field(obj, "metadata");
|
nn->metadata = eg_get_str_field(obj, "metadata");
|
||||||
if (!nn->metadata || !*nn->metadata) { free(nn->metadata); nn->metadata = el_strdup("{}"); }
|
if (!nn->metadata || !*nn->metadata) { free(nn->metadata); nn->metadata = el_strdup_persist("{}"); }
|
||||||
nn->salience = eg_get_num_field(obj, "salience");
|
nn->salience = eg_get_num_field(obj, "salience");
|
||||||
nn->importance = eg_get_num_field(obj, "importance");
|
nn->importance = eg_get_num_field(obj, "importance");
|
||||||
nn->confidence = eg_get_num_field(obj, "confidence");
|
nn->confidence = eg_get_num_field(obj, "confidence");
|
||||||
@@ -7424,6 +7770,14 @@ el_val_t engram_load(el_val_t path) {
|
|||||||
nn->updated_at = eg_get_int_field(obj, "updated_at");
|
nn->updated_at = eg_get_int_field(obj, "updated_at");
|
||||||
nn->background_activation = eg_get_num_field(obj, "background_activation");
|
nn->background_activation = eg_get_num_field(obj, "background_activation");
|
||||||
nn->working_memory_weight = eg_get_num_field(obj, "working_memory_weight");
|
nn->working_memory_weight = eg_get_num_field(obj, "working_memory_weight");
|
||||||
|
/* Launder persisted WM weights across restarts: snapshots carry
|
||||||
|
* legacy breakthrough-floor 0.25 weights (pinned by the old
|
||||||
|
* suppression-breakthrough path) and nothing else ever launders
|
||||||
|
* them. Halve on every boot so genuine working-memory state has
|
||||||
|
* continuity across a restart while stale pinned weights decay
|
||||||
|
* out over successive boots; sub-0.05 residue drops to zero. */
|
||||||
|
nn->working_memory_weight *= 0.5;
|
||||||
|
if (nn->working_memory_weight < 0.05) nn->working_memory_weight = 0.0;
|
||||||
nn->suppression_count = (int32_t)eg_get_int_field(obj, "suppression_count");
|
nn->suppression_count = (int32_t)eg_get_int_field(obj, "suppression_count");
|
||||||
/* layer_id defaults to ENGRAM_LAYER_DEFAULT (core-identity)
|
/* layer_id defaults to ENGRAM_LAYER_DEFAULT (core-identity)
|
||||||
* for snapshots that predate the layered schema. We can't
|
* for snapshots that predate the layered schema. We can't
|
||||||
@@ -7466,7 +7820,7 @@ el_val_t engram_load(el_val_t path) {
|
|||||||
ee->to_id = eg_get_str_field(obj, "to_id");
|
ee->to_id = eg_get_str_field(obj, "to_id");
|
||||||
ee->relation = eg_get_str_field(obj, "relation");
|
ee->relation = eg_get_str_field(obj, "relation");
|
||||||
ee->metadata = eg_get_str_field(obj, "metadata");
|
ee->metadata = eg_get_str_field(obj, "metadata");
|
||||||
if (!ee->metadata || !*ee->metadata) { free(ee->metadata); ee->metadata = el_strdup("{}"); }
|
if (!ee->metadata || !*ee->metadata) { free(ee->metadata); ee->metadata = el_strdup_persist("{}"); }
|
||||||
ee->weight = eg_get_num_field(obj, "weight");
|
ee->weight = eg_get_num_field(obj, "weight");
|
||||||
ee->confidence = eg_get_num_field(obj, "confidence");
|
ee->confidence = eg_get_num_field(obj, "confidence");
|
||||||
ee->created_at = eg_get_int_field(obj, "created_at");
|
ee->created_at = eg_get_int_field(obj, "created_at");
|
||||||
@@ -7541,6 +7895,9 @@ el_val_t engram_load(el_val_t path) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* WM cap discipline applies to every entry point that materializes WM,
|
||||||
|
* including snapshot restore (see eg_enforce_wm_cap_on_load). */
|
||||||
|
eg_enforce_wm_cap_on_load(g);
|
||||||
free(data);
|
free(data);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -7586,9 +7943,13 @@ el_val_t engram_load_merge(el_val_t path) {
|
|||||||
char* obj = malloc(n + 1);
|
char* obj = malloc(n + 1);
|
||||||
memcpy(obj, nodes_p, n); obj[n] = '\0';
|
memcpy(obj, nodes_p, n); obj[n] = '\0';
|
||||||
char* nid = eg_get_str_field(obj, "id");
|
char* nid = eg_get_str_field(obj, "id");
|
||||||
int already = (nid && *nid && engram_find_node(nid) != NULL);
|
/* Nodes with an empty/unparseable id can never dedup against
|
||||||
|
* the idmap, so without this guard they were re-added on EVERY
|
||||||
|
* merge cycle — unbounded store growth. Skip them entirely. */
|
||||||
|
int has_id = (nid && *nid);
|
||||||
|
int already = (has_id && engram_find_node(nid) != NULL);
|
||||||
free(nid);
|
free(nid);
|
||||||
if (!already) {
|
if (has_id && !already) {
|
||||||
engram_grow_nodes();
|
engram_grow_nodes();
|
||||||
EngramNode* nn = &g->nodes[g->node_count];
|
EngramNode* nn = &g->nodes[g->node_count];
|
||||||
memset(nn, 0, sizeof(*nn));
|
memset(nn, 0, sizeof(*nn));
|
||||||
@@ -7609,9 +7970,11 @@ el_val_t engram_load_merge(el_val_t path) {
|
|||||||
nn->created_at = eg_get_int_field(obj, "created_at");
|
nn->created_at = eg_get_int_field(obj, "created_at");
|
||||||
nn->updated_at = eg_get_int_field(obj, "updated_at");
|
nn->updated_at = eg_get_int_field(obj, "updated_at");
|
||||||
nn->background_activation = eg_get_num_field(obj, "background_activation");
|
nn->background_activation = eg_get_num_field(obj, "background_activation");
|
||||||
nn->working_memory_weight = eg_get_num_field(obj, "working_memory_weight");
|
/* Nodes arriving via merge were never part of THIS store's
|
||||||
if (!isfinite(nn->working_memory_weight) || nn->working_memory_weight < 0.0 || nn->working_memory_weight > 1.0)
|
* working memory — importing the source store's WM weight
|
||||||
nn->working_memory_weight = 0.0;
|
* would inject foreign (often legacy 0.25 breakthrough-
|
||||||
|
* floor) weights into local WM every sync cycle. */
|
||||||
|
nn->working_memory_weight = 0.0;
|
||||||
nn->suppression_count = (int32_t)eg_get_int_field(obj, "suppression_count");
|
nn->suppression_count = (int32_t)eg_get_int_field(obj, "suppression_count");
|
||||||
if (json_find_key(obj, "layer_id")) {
|
if (json_find_key(obj, "layer_id")) {
|
||||||
nn->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id");
|
nn->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id");
|
||||||
@@ -7694,6 +8057,9 @@ el_val_t engram_load_merge(el_val_t path) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Merged nodes can carry snapshot WM weights too — hold the cap here
|
||||||
|
* as well (see eg_enforce_wm_cap_on_load). */
|
||||||
|
eg_enforce_wm_cap_on_load(g);
|
||||||
free(data);
|
free(data);
|
||||||
return (el_val_t)added_nodes;
|
return (el_val_t)added_nodes;
|
||||||
}
|
}
|
||||||
@@ -7860,8 +8226,16 @@ el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t di
|
|||||||
free(frontier); free(frontier_h); free(visited);
|
free(frontier); free(frontier_h); free(visited);
|
||||||
jb_putc(&b, ']'); return el_wrap_str(b.buf);
|
jb_putc(&b, ']'); return el_wrap_str(b.buf);
|
||||||
}
|
}
|
||||||
frontier[fc] = el_strdup(sid); frontier_h[fc] = 0; fc++;
|
/* MUST be el_strdup_persist: this function frees frontier/visited strings
|
||||||
visited[vc++] = el_strdup(sid);
|
* manually (lines below). el_strdup would ALSO register them in the
|
||||||
|
* per-request arena, so el_request_end() double-freed every one of them
|
||||||
|
* at the end of the HTTP request — SIGABRT in http_worker under load.
|
||||||
|
* (2026-07-18 self-review; reproduced via ASAN on /api/neuron/session/begin
|
||||||
|
* and /api/neuron/graph. Same allocation-discipline class as the
|
||||||
|
* 2026-07-15 EngramNode and 2026-07-16 idmap-key fixes: never mix arena
|
||||||
|
* tracking with manual free.) */
|
||||||
|
frontier[fc] = el_strdup_persist(sid); frontier_h[fc] = 0; fc++;
|
||||||
|
visited[vc++] = el_strdup_persist(sid);
|
||||||
|
|
||||||
int first = 1;
|
int first = 1;
|
||||||
while (fc > 0) {
|
while (fc > 0) {
|
||||||
@@ -7890,8 +8264,8 @@ el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t di
|
|||||||
char tmp[64]; snprintf(tmp, sizeof(tmp), ",\"hops\":%lld}", (long long)(h + 1));
|
char tmp[64]; snprintf(tmp, sizeof(tmp), ",\"hops\":%lld}", (long long)(h + 1));
|
||||||
jb_puts(&b, tmp);
|
jb_puts(&b, tmp);
|
||||||
first = 0;
|
first = 0;
|
||||||
if (vc < 1024) visited[vc++] = el_strdup(peer);
|
if (vc < 1024) visited[vc++] = el_strdup_persist(peer);
|
||||||
if (fc < 1024 && h + 1 < depth) { frontier[fc] = el_strdup(peer); frontier_h[fc] = h + 1; fc++; }
|
if (fc < 1024 && h + 1 < depth) { frontier[fc] = el_strdup_persist(peer); frontier_h[fc] = h + 1; fc++; }
|
||||||
}
|
}
|
||||||
free(cur);
|
free(cur);
|
||||||
}
|
}
|
||||||
@@ -8612,13 +8986,13 @@ static int64_t dharma_find_or_create_relation_edge(const char* peer_base, int cr
|
|||||||
engram_grow_nodes();
|
engram_grow_nodes();
|
||||||
EngramNode* n = &g->nodes[g->node_count];
|
EngramNode* n = &g->nodes[g->node_count];
|
||||||
memset(n, 0, sizeof(*n));
|
memset(n, 0, sizeof(*n));
|
||||||
n->id = el_strdup(self_id);
|
n->id = el_strdup_persist(self_id);
|
||||||
n->content = el_strdup(_el_cgi_dharma_id ? _el_cgi_dharma_id : "(self)");
|
n->content = el_strdup_persist(_el_cgi_dharma_id ? _el_cgi_dharma_id : "(self)");
|
||||||
n->node_type = el_strdup("DharmaSelf");
|
n->node_type = el_strdup_persist("DharmaSelf");
|
||||||
n->label = el_strdup("dharma:self");
|
n->label = el_strdup_persist("dharma:self");
|
||||||
n->tier = el_strdup("Working");
|
n->tier = el_strdup_persist("Working");
|
||||||
n->tags = el_strdup("dharma");
|
n->tags = el_strdup_persist("dharma");
|
||||||
n->metadata = el_strdup("{}");
|
n->metadata = el_strdup_persist("{}");
|
||||||
n->salience = 1.0; n->importance = 1.0; n->confidence = 1.0;
|
n->salience = 1.0; n->importance = 1.0; n->confidence = 1.0;
|
||||||
int64_t now = engram_now_ms();
|
int64_t now = engram_now_ms();
|
||||||
n->created_at = now; n->updated_at = now; n->last_activated = now;
|
n->created_at = now; n->updated_at = now; n->last_activated = now;
|
||||||
@@ -8629,13 +9003,13 @@ static int64_t dharma_find_or_create_relation_edge(const char* peer_base, int cr
|
|||||||
engram_grow_nodes();
|
engram_grow_nodes();
|
||||||
EngramNode* n = &g->nodes[g->node_count];
|
EngramNode* n = &g->nodes[g->node_count];
|
||||||
memset(n, 0, sizeof(*n));
|
memset(n, 0, sizeof(*n));
|
||||||
n->id = el_strdup(peer_node);
|
n->id = el_strdup_persist(peer_node);
|
||||||
n->content = el_strdup(peer_base);
|
n->content = el_strdup_persist(peer_base);
|
||||||
n->node_type = el_strdup("DharmaPeer");
|
n->node_type = el_strdup_persist("DharmaPeer");
|
||||||
n->label = el_strdup(peer_node);
|
n->label = el_strdup_persist(peer_node);
|
||||||
n->tier = el_strdup("Working");
|
n->tier = el_strdup_persist("Working");
|
||||||
n->tags = el_strdup("dharma");
|
n->tags = el_strdup_persist("dharma");
|
||||||
n->metadata = el_strdup("{}");
|
n->metadata = el_strdup_persist("{}");
|
||||||
n->salience = 0.5; n->importance = 0.5; n->confidence = 1.0;
|
n->salience = 0.5; n->importance = 0.5; n->confidence = 1.0;
|
||||||
int64_t now = engram_now_ms();
|
int64_t now = engram_now_ms();
|
||||||
n->created_at = now; n->updated_at = now; n->last_activated = now;
|
n->created_at = now; n->updated_at = now; n->last_activated = now;
|
||||||
@@ -8647,10 +9021,10 @@ static int64_t dharma_find_or_create_relation_edge(const char* peer_base, int cr
|
|||||||
EngramEdge* e = &g->edges[g->edge_count];
|
EngramEdge* e = &g->edges[g->edge_count];
|
||||||
memset(e, 0, sizeof(*e));
|
memset(e, 0, sizeof(*e));
|
||||||
e->id = engram_new_id();
|
e->id = engram_new_id();
|
||||||
e->from_id = el_strdup(self_id);
|
e->from_id = el_strdup_persist(self_id);
|
||||||
e->to_id = el_strdup(peer_node);
|
e->to_id = el_strdup_persist(peer_node);
|
||||||
e->relation = el_strdup("dharma-relation");
|
e->relation = el_strdup_persist("dharma-relation");
|
||||||
e->metadata = el_strdup("{}");
|
e->metadata = el_strdup_persist("{}");
|
||||||
e->weight = 0.0;
|
e->weight = 0.0;
|
||||||
e->confidence = 1.0;
|
e->confidence = 1.0;
|
||||||
int64_t now = engram_now_ms();
|
int64_t now = engram_now_ms();
|
||||||
|
|||||||
Reference in New Issue
Block a user