Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f8ccc945e | |||
| 409ec99397 | |||
| dc39a61e2c | |||
| eba9eac8a8 | |||
| ab6b52a0b4 | |||
| e3dabe3e08 | |||
| 0a0a2bcb44 |
Vendored
BIN
Binary file not shown.
Vendored
+140
-93
@@ -10,6 +10,7 @@ el_val_t query_param(el_val_t path, el_val_t key);
|
|||||||
el_val_t query_int(el_val_t path, el_val_t key, el_val_t default_val);
|
el_val_t query_int(el_val_t path, el_val_t key, el_val_t default_val);
|
||||||
el_val_t extract_id(el_val_t path, el_val_t prefix);
|
el_val_t extract_id(el_val_t path, el_val_t prefix);
|
||||||
el_val_t route_stats(el_val_t method, el_val_t path, el_val_t body);
|
el_val_t route_stats(el_val_t method, el_val_t path, el_val_t body);
|
||||||
|
el_val_t persist_canonical(void);
|
||||||
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 route_get_node(el_val_t method, el_val_t path, el_val_t body);
|
el_val_t route_get_node(el_val_t method, el_val_t path, el_val_t body);
|
||||||
el_val_t route_scan_nodes(el_val_t method, el_val_t path, el_val_t body);
|
el_val_t route_scan_nodes(el_val_t method, el_val_t path, el_val_t body);
|
||||||
@@ -20,18 +21,23 @@ 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_load_merge(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;
|
||||||
|
el_val_t boot_snap;
|
||||||
|
|
||||||
el_val_t parse_port(el_val_t bind) {
|
el_val_t parse_port(el_val_t bind) {
|
||||||
el_val_t colon = str_index_of(bind, EL_STR(":"));
|
el_val_t colon = str_index_of(bind, EL_STR(":"));
|
||||||
@@ -110,17 +116,22 @@ el_val_t route_stats(el_val_t method, el_val_t path, el_val_t body) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
el_val_t persist_canonical(void) {
|
||||||
|
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||||
|
el_val_t dir = ({ el_val_t _if_result_1 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_1 = (EL_STR("/tmp/engram")); } else { _if_result_1 = (dir_raw); } _if_result_1; });
|
||||||
|
engram_save(el_str_concat(dir, EL_STR("/snapshot.json")));
|
||||||
|
return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
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_2 = 0; if (str_eq(nt_raw, EL_STR(""))) { _if_result_2 = (EL_STR("Memory")); } else { _if_result_2 = (nt_raw); } _if_result_2; });
|
||||||
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_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 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);
|
||||||
|
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 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,11 +157,9 @@ 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_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; });
|
||||||
dir = EL_STR("/tmp/engram");
|
el_val_t snap_path = el_str_concat(dir, EL_STR("/.scan-export.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);
|
||||||
if (str_eq(snap, EL_STR(""))) {
|
if (str_eq(snap, EL_STR(""))) {
|
||||||
@@ -165,36 +174,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_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; });
|
||||||
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_6 = 0; if ((lim_url > 0)) { _if_result_6 = (lim_url); } else { _if_result_6 = (lim_body); } _if_result_6; });
|
||||||
q = json_get_string(body, EL_STR("query"));
|
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 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_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 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_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; });
|
||||||
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,15 +197,12 @@ 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_11 = 0; if (str_eq(rel_raw, EL_STR(""))) { _if_result_11 = (EL_STR("associates")); } else { _if_result_11 = (rel_raw); } _if_result_11; });
|
||||||
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_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 = 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);
|
||||||
|
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("\"}"));
|
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;
|
||||||
}
|
}
|
||||||
@@ -231,6 +223,7 @@ el_val_t route_strengthen(el_val_t method, el_val_t path, el_val_t body) {
|
|||||||
return err_json(EL_STR("missing node_id"));
|
return err_json(EL_STR("missing node_id"));
|
||||||
}
|
}
|
||||||
engram_strengthen(id);
|
engram_strengthen(id);
|
||||||
|
el_val_t saved = persist_canonical();
|
||||||
return ok_json();
|
return ok_json();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -241,29 +234,40 @@ el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body) {
|
|||||||
return err_json(EL_STR("missing id"));
|
return err_json(EL_STR("missing id"));
|
||||||
}
|
}
|
||||||
engram_forget(id);
|
engram_forget(id);
|
||||||
|
el_val_t saved = persist_canonical();
|
||||||
return ok_json();
|
return ok_json();
|
||||||
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_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; });
|
||||||
|
engram_save(p);
|
||||||
|
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"path\":\""), p), EL_STR("\"}"));
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
el_val_t sal = el_from_float(0.3);
|
|
||||||
el_val_t imp = el_from_float(0.3);
|
el_val_t route_load(el_val_t method, el_val_t path, el_val_t body) {
|
||||||
el_val_t conf = el_from_float(0.8);
|
el_val_t p_raw = json_get_string(body, EL_STR("path"));
|
||||||
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 dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||||
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), id), EL_STR("\"}"));
|
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; });
|
||||||
|
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_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; });
|
||||||
dir = EL_STR("/tmp/engram");
|
el_val_t snap_path = el_str_concat(dir, EL_STR("/.sync-export.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 +277,68 @@ 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_load_merge(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 p = json_get_string(body, EL_STR("path"));
|
||||||
if (str_eq(p, EL_STR(""))) {
|
if (str_eq(p, EL_STR(""))) {
|
||||||
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
|
return err_json(EL_STR("path is required"));
|
||||||
if (str_eq(dir, EL_STR(""))) {
|
|
||||||
dir = EL_STR("/tmp/engram");
|
|
||||||
}
|
}
|
||||||
p = el_str_concat(dir, EL_STR("/snapshot.json"));
|
if (str_eq(fs_read(p), EL_STR(""))) {
|
||||||
|
return err_json(EL_STR("file missing or empty"));
|
||||||
}
|
}
|
||||||
engram_save(p);
|
el_val_t before_n = engram_node_count();
|
||||||
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"path\":\""), p), EL_STR("\"}"));
|
el_val_t before_e = engram_edge_count();
|
||||||
|
engram_load_merge(p);
|
||||||
|
el_val_t added_n = (engram_node_count() - before_n);
|
||||||
|
el_val_t added_e = (engram_edge_count() - before_e);
|
||||||
|
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,\"nodes_added\":"), int_to_str(added_n)), EL_STR(",\"edges_added\":")), int_to_str(added_e)), EL_STR(",\"node_count\":")), int_to_str(engram_node_count())), 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_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"));
|
el_val_t sal = el_from_float(0.3);
|
||||||
}
|
el_val_t imp = el_from_float(0.3);
|
||||||
engram_load(p);
|
el_val_t conf = el_from_float(0.8);
|
||||||
return ok_json();
|
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 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_health(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) {
|
||||||
return EL_STR("{\"status\":\"ok\",\"engine\":\"engram-runtime-native\"}");
|
el_val_t content = json_get_string(body, EL_STR("content"));
|
||||||
|
if (str_eq(content, EL_STR(""))) {
|
||||||
|
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 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 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 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 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 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 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);
|
||||||
|
el_val_t saved = persist_canonical();
|
||||||
|
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), id), EL_STR("\"}"));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,12 +365,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,32 +413,40 @@ 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("POST")) && (str_eq(clean, EL_STR("/api/load-merge")) || str_eq(clean, EL_STR("/load-merge")))) {
|
||||||
|
return route_load_merge(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_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_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_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_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);
|
||||||
|
boot_snap = fs_read(snapshot_path);
|
||||||
|
if (!str_eq(boot_snap, EL_STR(""))) {
|
||||||
|
if (engram_node_count() == 0) {
|
||||||
|
println(EL_STR("[engram] WARNING: snapshot.json is non-empty but load produced 0 nodes \xe2\x80\x94 preserving copy at snapshot.failed-load.json"));
|
||||||
|
fs_write(el_str_concat(data_dir, EL_STR("/snapshot.failed-load.json")), boot_snap);
|
||||||
|
} else {
|
||||||
|
fs_write(el_str_concat(data_dir, EL_STR("/snapshot.boot-backup.json")), boot_snap);
|
||||||
|
}
|
||||||
|
}
|
||||||
println(EL_STR("[engram] runtime-native graph engine"));
|
println(EL_STR("[engram] runtime-native graph engine"));
|
||||||
println(el_str_concat(EL_STR("[engram] data_dir="), data_dir));
|
println(el_str_concat(EL_STR("[engram] data_dir="), data_dir));
|
||||||
println(el_str_concat(EL_STR("[engram] node_count="), int_to_str(engram_node_count())));
|
println(el_str_concat(EL_STR("[engram] node_count="), int_to_str(engram_node_count())));
|
||||||
|
|||||||
+190
-54
@@ -76,13 +76,43 @@ 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).
|
||||||
|
// persist_canonical — save the canonical snapshot after a durable write.
|
||||||
|
//
|
||||||
|
// WHY (2026-07-22 self-review): the 2026-07-21 fix correctly stopped READ
|
||||||
|
// routes from writing the canonical snapshot.json — but nothing was left
|
||||||
|
// that saved it on WRITE. Every mutation (node create, edge create,
|
||||||
|
// knowledge capture, forget, merge) lived only in RAM until someone POSTed
|
||||||
|
// /api/save manually; a process restart silently discarded everything since
|
||||||
|
// the last manual save. Observed live: two engram restarts during the
|
||||||
|
// 2026-07-22 review reverted the store to a ~17h-old snapshot, destroying
|
||||||
|
// same-day writes. Reads must never write the canonical; writes must always
|
||||||
|
// persist it. ISE telemetry is deliberately excluded (48h-pruned, loss-
|
||||||
|
// tolerant, ~2/min — snapshotting the whole store per heartbeat is waste;
|
||||||
|
// any durable write that follows persists the pruning too).
|
||||||
|
fn persist_canonical() -> Int {
|
||||||
|
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||||
|
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||||||
|
engram_save(dir + "/snapshot.json")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
fn route_create_node(method: String, path: String, body: String) -> String {
|
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)
|
||||||
|
let saved: Int = persist_canonical()
|
||||||
"{\"id\":\"" + id + "\",\"content\":\"" + content + "\",\"node_type\":\"" + node_type + "\"}"
|
"{\"id\":\"" + id + "\",\"content\":\"" + content + "\",\"node_type\":\"" + node_type + "\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,13 +133,14 @@ fn route_scan_nodes(method: String, path: String, body: String) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// route_scan_edges — bulk export of all edges as a JSON array. Implemented
|
// route_scan_edges — bulk export of all edges as a JSON array. Implemented
|
||||||
// via engram_save → fs_read of the canonical on-disk snapshot, which the
|
// via engram_save → fs_read of a SCRATCH export path. (2026-07-21 self-review:
|
||||||
// runtime keeps in lockstep with the in-memory graph. Live against the
|
// previously this saved over the canonical snapshot.json on every GET — if the
|
||||||
// running graph, not a stale export.
|
// process ever booted with a partial/empty store, the first read request
|
||||||
|
// clobbered the good snapshot. Read routes must never write the canonical path.)
|
||||||
fn route_scan_edges(method: String, path: String, body: String) -> String {
|
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 + "/.scan-export.json"
|
||||||
engram_save(snap_path)
|
engram_save(snap_path)
|
||||||
let snap: String = fs_read(snap_path)
|
let snap: String = fs_read(snap_path)
|
||||||
if str_eq(snap, "") { return "[]" }
|
if str_eq(snap, "") { return "[]" }
|
||||||
@@ -122,40 +153,34 @@ 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)
|
||||||
|
let saved: Int = persist_canonical()
|
||||||
"{\"ok\":true,\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + relation + "\"}"
|
"{\"ok\":true,\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + relation + "\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +195,7 @@ fn route_strengthen(method: String, path: String, body: String) -> String {
|
|||||||
let id: String = json_get_string(body, "node_id")
|
let id: String = json_get_string(body, "node_id")
|
||||||
if str_eq(id, "") { return err_json("missing node_id") }
|
if str_eq(id, "") { return err_json("missing node_id") }
|
||||||
engram_strengthen(id)
|
engram_strengthen(id)
|
||||||
|
let saved: Int = persist_canonical()
|
||||||
ok_json()
|
ok_json()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,27 +203,24 @@ fn route_forget(method: String, path: String, body: String) -> String {
|
|||||||
let id: String = extract_id(path, "/api/nodes/")
|
let id: String = extract_id(path, "/api/nodes/")
|
||||||
if str_eq(id, "") { return err_json("missing id") }
|
if str_eq(id, "") { return err_json("missing id") }
|
||||||
engram_forget(id)
|
engram_forget(id)
|
||||||
|
let saved: Int = persist_canonical()
|
||||||
ok_json()
|
ok_json()
|
||||||
}
|
}
|
||||||
|
|
||||||
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,15 +242,36 @@ 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"
|
// 2026-07-21 self-review: export to a scratch path, never the canonical
|
||||||
|
// snapshot.json — read routes must not be able to clobber the good snapshot.
|
||||||
|
let snap_path: String = dir + "/.sync-export.json"
|
||||||
engram_save(snap_path)
|
engram_save(snap_path)
|
||||||
let snap: String = fs_read(snap_path)
|
let snap: String = fs_read(snap_path)
|
||||||
if str_eq(snap, "") { return "{\"nodes\":[],\"edges\":[]}" }
|
if str_eq(snap, "") { return "{\"nodes\":[],\"edges\":[]}" }
|
||||||
return snap
|
return snap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// route_load_merge — POST /api/load-merge {"path": "..."} — merge a snapshot
|
||||||
|
// file into the live store WITHOUT resetting it (engram_load_merge skips nodes
|
||||||
|
// already present by id). Added 2026-07-21 self-review to restore the 244 kn-
|
||||||
|
// identity Knowledge nodes lost from the snapshot lineage between 05-13 and
|
||||||
|
// 07-13. Requires an explicit path: refuses to run without one so it can never
|
||||||
|
// be triggered accidentally against a default.
|
||||||
|
fn route_load_merge(method: String, path: String, body: String) -> String {
|
||||||
|
let p: String = json_get_string(body, "path")
|
||||||
|
if str_eq(p, "") { return err_json("path is required") }
|
||||||
|
if str_eq(fs_read(p), "") { return err_json("file missing or empty") }
|
||||||
|
let before_n: Int = engram_node_count()
|
||||||
|
let before_e: Int = engram_edge_count()
|
||||||
|
engram_load_merge(p)
|
||||||
|
let added_n: Int = engram_node_count() - before_n
|
||||||
|
let added_e: Int = engram_edge_count() - before_e
|
||||||
|
let saved: Int = persist_canonical()
|
||||||
|
"{\"ok\":true,\"nodes_added\":" + int_to_str(added_n) + ",\"edges_added\":" + int_to_str(added_e) + ",\"node_count\":" + int_to_str(engram_node_count()) + "}"
|
||||||
|
}
|
||||||
|
|
||||||
// route_emit_ise — write an InternalStateEvent node from the soul daemon.
|
// route_emit_ise — write an InternalStateEvent node from the soul daemon.
|
||||||
//
|
//
|
||||||
// Endpoint: POST /api/neuron/state-events
|
// Endpoint: POST /api/neuron/state-events
|
||||||
@@ -241,10 +285,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 +310,64 @@ 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
|
||||||
|
)
|
||||||
|
let saved: Int = persist_canonical()
|
||||||
"{\"ok\":true,\"id\":\"" + id + "\"}"
|
"{\"ok\":true,\"id\":\"" + id + "\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,6 +407,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)
|
||||||
@@ -351,6 +469,9 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
|||||||
if str_eq(method, "POST") && (str_eq(clean, "/api/load") || str_eq(clean, "/load")) {
|
if str_eq(method, "POST") && (str_eq(clean, "/api/load") || str_eq(clean, "/load")) {
|
||||||
return route_load(method, path, body)
|
return route_load(method, path, body)
|
||||||
}
|
}
|
||||||
|
if str_eq(method, "POST") && (str_eq(clean, "/api/load-merge") || str_eq(clean, "/load-merge")) {
|
||||||
|
return route_load_merge(method, path, body)
|
||||||
|
}
|
||||||
|
|
||||||
// Sync — soul daemon periodic pull of non-ISE knowledge into in-process graph
|
// Sync — soul daemon periodic pull of non-ISE knowledge into in-process graph
|
||||||
if str_eq(method, "GET") && str_eq(clean, "/api/sync") {
|
if str_eq(method, "GET") && str_eq(clean, "/api/sync") {
|
||||||
@@ -362,16 +483,31 @@ 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)
|
||||||
|
|
||||||
|
// 2026-07-21 self-review boot guard: if the snapshot file has content but the
|
||||||
|
// load produced 0 nodes, something is wrong (corrupt file / parse failure).
|
||||||
|
// Preserve the evidence and warn loudly — and since read routes no longer write
|
||||||
|
// the canonical path, a bad boot can no longer clobber the good snapshot.
|
||||||
|
let boot_snap: String = fs_read(snapshot_path)
|
||||||
|
if !str_eq(boot_snap, "") {
|
||||||
|
if engram_node_count() == 0 {
|
||||||
|
println("[engram] WARNING: snapshot.json is non-empty but load produced 0 nodes — preserving copy at snapshot.failed-load.json")
|
||||||
|
fs_write(data_dir + "/snapshot.failed-load.json", boot_snap)
|
||||||
|
} else {
|
||||||
|
// Good load: keep a boot-time backup of the snapshot as loaded.
|
||||||
|
fs_write(data_dir + "/snapshot.boot-backup.json", boot_snap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
println("[engram] runtime-native graph engine")
|
println("[engram] runtime-native graph engine")
|
||||||
println("[engram] data_dir=" + data_dir)
|
println("[engram] data_dir=" + data_dir)
|
||||||
println("[engram] node_count=" + int_to_str(engram_node_count()))
|
println("[engram] node_count=" + int_to_str(engram_node_count()))
|
||||||
|
|||||||
@@ -6826,6 +6826,75 @@ static int istr_contains(const char* hay, const char* needle) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Tokenized query matching ───────────────────────────────────────────
|
||||||
|
* The engram query surface (search / activate / goal-bias) historically
|
||||||
|
* matched the ENTIRE raw query string as a single case-insensitive
|
||||||
|
* substring via istr_contains(field, q). That is Ctrl-F, not search:
|
||||||
|
* a multi-word query like "windows msi signing" only matched a node whose
|
||||||
|
* text contained that exact contiguous run, so real multi-word queries
|
||||||
|
* returned zero. istr_contains stays as the per-TOKEN primitive; these
|
||||||
|
* helpers split the query on whitespace and match ANY token, then rank by
|
||||||
|
* how many DISTINCT tokens a node covers. Single-token queries are a strict
|
||||||
|
* special case (score is 0 or 1) so single-word callers never regress. */
|
||||||
|
#define ENGRAM_MAX_QTOKENS 32
|
||||||
|
#define ENGRAM_QTOK_LEN 256
|
||||||
|
|
||||||
|
/* Split q on whitespace into up to ENGRAM_MAX_QTOKENS distinct
|
||||||
|
* (case-insensitive) tokens. Returns the token count. Over-long tokens are
|
||||||
|
* truncated to ENGRAM_QTOK_LEN-1; over-count tokens are ignored. */
|
||||||
|
static int engram_tokenize_query(const char* q,
|
||||||
|
char toks[][ENGRAM_QTOK_LEN], int maxtok) {
|
||||||
|
int n = 0;
|
||||||
|
if (!q) return 0;
|
||||||
|
const char* p = q;
|
||||||
|
while (*p && n < maxtok) {
|
||||||
|
while (*p && isspace((unsigned char)*p)) p++;
|
||||||
|
if (!*p) break;
|
||||||
|
char buf[ENGRAM_QTOK_LEN];
|
||||||
|
size_t tl = 0;
|
||||||
|
while (*p && !isspace((unsigned char)*p)) {
|
||||||
|
if (tl < sizeof(buf) - 1) buf[tl++] = *p;
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
buf[tl] = '\0';
|
||||||
|
if (tl == 0) continue;
|
||||||
|
int dup = 0;
|
||||||
|
for (int s = 0; s < n; s++) {
|
||||||
|
if (strcasecmp(toks[s], buf) == 0) { dup = 1; break; }
|
||||||
|
}
|
||||||
|
if (dup) continue;
|
||||||
|
memcpy(toks[n], buf, tl + 1);
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Count how many of the ntok distinct query tokens appear (case-insensitive)
|
||||||
|
* in the node's content, label, or tags. 0 == no match. */
|
||||||
|
static int engram_node_match_score(const EngramNode* n,
|
||||||
|
char toks[][ENGRAM_QTOK_LEN], int ntok) {
|
||||||
|
int score = 0;
|
||||||
|
for (int t = 0; t < ntok; t++) {
|
||||||
|
if (istr_contains(n->content, toks[t]) ||
|
||||||
|
istr_contains(n->label, toks[t]) ||
|
||||||
|
istr_contains(n->tags, toks[t]))
|
||||||
|
score++;
|
||||||
|
}
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rank entry: distinct-token match count (primary, desc) then salience
|
||||||
|
* (tiebreak, desc). */
|
||||||
|
typedef struct { int64_t idx; int score; double salience; } EngramRankEntry;
|
||||||
|
static int engram_rank_cmp(const void* a, const void* b) {
|
||||||
|
const EngramRankEntry* ea = (const EngramRankEntry*)a;
|
||||||
|
const EngramRankEntry* eb = (const EngramRankEntry*)b;
|
||||||
|
if (ea->score != eb->score) return eb->score - ea->score; /* desc */
|
||||||
|
if (ea->salience < eb->salience) return 1;
|
||||||
|
if (ea->salience > eb->salience) return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
el_val_t engram_search(el_val_t query, el_val_t limit) {
|
el_val_t engram_search(el_val_t query, el_val_t limit) {
|
||||||
EngramStore* g = engram_get();
|
EngramStore* g = engram_get();
|
||||||
const char* q = EL_CSTR(query);
|
const char* q = EL_CSTR(query);
|
||||||
@@ -6833,21 +6902,34 @@ el_val_t engram_search(el_val_t query, el_val_t limit) {
|
|||||||
if (lim <= 0) lim = 100;
|
if (lim <= 0) lim = 100;
|
||||||
el_val_t lst = el_list_empty();
|
el_val_t lst = el_list_empty();
|
||||||
if (!q || !*q) return lst;
|
if (!q || !*q) return lst;
|
||||||
int64_t found = 0;
|
char toks[ENGRAM_MAX_QTOKENS][ENGRAM_QTOK_LEN];
|
||||||
for (int64_t i = 0; i < g->node_count && found < lim; i++) {
|
int ntok = engram_tokenize_query(q, toks, ENGRAM_MAX_QTOKENS);
|
||||||
|
if (ntok == 0) return lst;
|
||||||
|
EngramRankEntry* hits = malloc((size_t)g->node_count * sizeof(EngramRankEntry));
|
||||||
|
if (!hits) return lst;
|
||||||
|
int64_t nhits = 0;
|
||||||
|
for (int64_t i = 0; i < g->node_count; i++) {
|
||||||
EngramNode* n = &g->nodes[i];
|
EngramNode* n = &g->nodes[i];
|
||||||
/* Filter transparent layers: nodes whose layer is `transparent=1`
|
/* Filter transparent layers: nodes whose layer is `transparent=1`
|
||||||
* shape output but are invisible to introspection ("what do you
|
* shape output but are invisible to introspection ("what do you
|
||||||
* know about yourself"). They still surface via engram_activate
|
* know about yourself"). They still surface via engram_activate
|
||||||
* + engram_compile_layered_json — that's the legitimate path. */
|
* + engram_compile_layered_json — that's the legitimate path. */
|
||||||
if (engram_layer_is_transparent(n->layer_id)) continue;
|
if (engram_layer_is_transparent(n->layer_id)) continue;
|
||||||
if (istr_contains(n->content, q) ||
|
int sc = engram_node_match_score(n, toks, ntok);
|
||||||
istr_contains(n->label, q) ||
|
if (sc > 0) {
|
||||||
istr_contains(n->tags, q)) {
|
hits[nhits].idx = i;
|
||||||
lst = el_list_append(lst, engram_node_to_map(n));
|
hits[nhits].score = sc;
|
||||||
found++;
|
hits[nhits].salience = n->salience;
|
||||||
|
nhits++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* Rank by distinct tokens matched (desc) then salience (desc), then cap. */
|
||||||
|
qsort(hits, (size_t)nhits, sizeof(EngramRankEntry), engram_rank_cmp);
|
||||||
|
int64_t end = nhits < lim ? nhits : lim;
|
||||||
|
for (int64_t k = 0; k < end; k++) {
|
||||||
|
lst = el_list_append(lst, engram_node_to_map(&g->nodes[hits[k].idx]));
|
||||||
|
}
|
||||||
|
free(hits);
|
||||||
return lst;
|
return lst;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7124,10 +7206,14 @@ static double engram_temporal_proximity_bonus(int64_t node_created,
|
|||||||
static double engram_goal_bias(const EngramNode* n, const char* query) {
|
static double engram_goal_bias(const EngramNode* n, const char* query) {
|
||||||
if (!query || !*query) return 1.0;
|
if (!query || !*query) return 1.0;
|
||||||
double bias = 1.0;
|
double bias = 1.0;
|
||||||
/* Direct lexical overlap: node content/label/tags share text with query. */
|
/* Direct lexical overlap, graded by token coverage: a node covering all
|
||||||
if (istr_contains(n->content, query) || istr_contains(n->label, query) ||
|
* query tokens gets the full +0.5; partial coverage gets a proportional
|
||||||
istr_contains(n->tags, query)) {
|
* share. Single-token queries → full +0.5 on match, identical to before. */
|
||||||
bias += 0.5;
|
{
|
||||||
|
char toks[ENGRAM_MAX_QTOKENS][ENGRAM_QTOK_LEN];
|
||||||
|
int ntok = engram_tokenize_query(query, toks, ENGRAM_MAX_QTOKENS);
|
||||||
|
int sc = engram_node_match_score(n, toks, ntok);
|
||||||
|
if (sc > 0 && ntok > 0) bias += 0.5 * ((double)sc / (double)ntok);
|
||||||
}
|
}
|
||||||
/* Node-type resonance with query intent. */
|
/* Node-type resonance with query intent. */
|
||||||
int technical_query = istr_contains(query, "code") ||
|
int technical_query = istr_contains(query, "code") ||
|
||||||
@@ -7193,14 +7279,21 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
|||||||
if (!seeds) {
|
if (!seeds) {
|
||||||
free(best_bg); free(best_hops); free(reached); return out;
|
free(best_bg); free(best_hops); free(reached); 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
|
||||||
|
* tokens it contains) so a node matching all words seeds more strongly
|
||||||
|
* than one matching a single word. Single-word queries → coverage 1.0,
|
||||||
|
* identical to the prior whole-query behavior. */
|
||||||
|
char toks[ENGRAM_MAX_QTOKENS][ENGRAM_QTOK_LEN];
|
||||||
|
int ntok = engram_tokenize_query(q, toks, ENGRAM_MAX_QTOKENS);
|
||||||
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];
|
||||||
if (istr_contains(n->content, q) ||
|
int sc = engram_node_match_score(n, toks, ntok);
|
||||||
istr_contains(n->label, q) ||
|
if (sc > 0) {
|
||||||
istr_contains(n->tags, q)) {
|
|
||||||
double tdecay = engram_temporal_decay(n, now_ms);
|
double tdecay = engram_temporal_decay(n, now_ms);
|
||||||
double dampen = engram_activation_dampen(n);
|
double dampen = engram_activation_dampen(n);
|
||||||
double act = n->salience * tdecay * dampen;
|
double cover = ntok > 0 ? (double)sc / (double)ntok : 1.0;
|
||||||
|
double act = n->salience * tdecay * dampen * cover;
|
||||||
seeds[seed_count].idx = i;
|
seeds[seed_count].idx = i;
|
||||||
seeds[seed_count].act = act;
|
seeds[seed_count].act = act;
|
||||||
seeds[seed_count].created_at = n->created_at;
|
seeds[seed_count].created_at = n->created_at;
|
||||||
@@ -7761,35 +7854,6 @@ el_val_t engram_get_node_json(el_val_t id) {
|
|||||||
return el_wrap_str(jb_finish(&b));
|
return el_wrap_str(jb_finish(&b));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* engram_get_node_by_label — find the first node whose label field exactly
|
|
||||||
* matches the given string. Returns the node as a JSON object string, or "{}"
|
|
||||||
* if no match is found.
|
|
||||||
*
|
|
||||||
* Used by chat.el to retrieve well-known nodes (e.g. "conv:history",
|
|
||||||
* "session:summary") by their stable label rather than by ID, which is immune
|
|
||||||
* to vector index drift across restarts.
|
|
||||||
*
|
|
||||||
* Exact match (strcmp, not istr_contains) because labels like "conv:history"
|
|
||||||
* must not collide with nodes whose content happens to contain that substring.
|
|
||||||
*
|
|
||||||
* Backported verbatim (idiom-adapted to jb_finish) from release runtime
|
|
||||||
* v1.0.0-20260501 to unblock the soul regen link: chat.el references this
|
|
||||||
* native but the current runtime lacked its definition. */
|
|
||||||
el_val_t engram_get_node_by_label(el_val_t label) {
|
|
||||||
const char* lbl = EL_CSTR(label);
|
|
||||||
if (!lbl || !*lbl) return el_wrap_str(el_strdup("{}"));
|
|
||||||
EngramStore* g = engram_get();
|
|
||||||
for (int64_t i = 0; i < g->node_count; i++) {
|
|
||||||
EngramNode* n = &g->nodes[i];
|
|
||||||
if (n->label && strcmp(n->label, lbl) == 0) {
|
|
||||||
JsonBuf b; jb_init(&b);
|
|
||||||
engram_emit_node_json(&b, n);
|
|
||||||
return el_wrap_str(jb_finish(&b));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return el_wrap_str(el_strdup("{}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
el_val_t engram_search_json(el_val_t query, el_val_t limit) {
|
el_val_t engram_search_json(el_val_t query, el_val_t limit) {
|
||||||
EngramStore* g = engram_get();
|
EngramStore* g = engram_get();
|
||||||
const char* q = EL_CSTR(query);
|
const char* q = EL_CSTR(query);
|
||||||
@@ -7798,19 +7862,36 @@ el_val_t engram_search_json(el_val_t query, el_val_t limit) {
|
|||||||
JsonBuf b; jb_init(&b);
|
JsonBuf b; jb_init(&b);
|
||||||
jb_putc(&b, '[');
|
jb_putc(&b, '[');
|
||||||
int first = 1;
|
int first = 1;
|
||||||
int64_t found = 0;
|
|
||||||
if (q && *q) {
|
if (q && *q) {
|
||||||
for (int64_t i = 0; i < g->node_count && found < lim; i++) {
|
char toks[ENGRAM_MAX_QTOKENS][ENGRAM_QTOK_LEN];
|
||||||
|
int ntok = engram_tokenize_query(q, toks, ENGRAM_MAX_QTOKENS);
|
||||||
|
if (ntok > 0) {
|
||||||
|
EngramRankEntry* hits =
|
||||||
|
malloc((size_t)g->node_count * sizeof(EngramRankEntry));
|
||||||
|
if (hits) {
|
||||||
|
int64_t nhits = 0;
|
||||||
|
for (int64_t i = 0; i < g->node_count; i++) {
|
||||||
EngramNode* n = &g->nodes[i];
|
EngramNode* n = &g->nodes[i];
|
||||||
/* Filter transparent layers — same as engram_search. */
|
/* Filter transparent layers — same as engram_search. */
|
||||||
if (engram_layer_is_transparent(n->layer_id)) continue;
|
if (engram_layer_is_transparent(n->layer_id)) continue;
|
||||||
if (istr_contains(n->content, q) ||
|
int sc = engram_node_match_score(n, toks, ntok);
|
||||||
istr_contains(n->label, q) ||
|
if (sc > 0) {
|
||||||
istr_contains(n->tags, q)) {
|
hits[nhits].idx = i;
|
||||||
|
hits[nhits].score = sc;
|
||||||
|
hits[nhits].salience = n->salience;
|
||||||
|
nhits++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Rank by distinct tokens matched (desc) then salience (desc). */
|
||||||
|
qsort(hits, (size_t)nhits, sizeof(EngramRankEntry),
|
||||||
|
engram_rank_cmp);
|
||||||
|
int64_t end = nhits < lim ? nhits : lim;
|
||||||
|
for (int64_t k = 0; k < end; k++) {
|
||||||
if (!first) jb_putc(&b, ',');
|
if (!first) jb_putc(&b, ',');
|
||||||
engram_emit_node_json(&b, n);
|
engram_emit_node_json(&b, &g->nodes[hits[k].idx]);
|
||||||
first = 0;
|
first = 0;
|
||||||
found++;
|
}
|
||||||
|
free(hits);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -632,7 +632,6 @@ el_val_t engram_load(el_val_t path);
|
|||||||
* can pass results straight through without round-tripping ElList/ElMap
|
* can pass results straight through without round-tripping ElList/ElMap
|
||||||
* through json_stringify. */
|
* through json_stringify. */
|
||||||
el_val_t engram_get_node_json(el_val_t id);
|
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_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_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_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
|
||||||
|
|||||||
@@ -1072,7 +1072,6 @@ el_val_t __engram_save(el_val_t path) { return engram_save
|
|||||||
el_val_t __engram_load(el_val_t path) { return engram_load(path); }
|
el_val_t __engram_load(el_val_t path) { return engram_load(path); }
|
||||||
|
|
||||||
el_val_t __engram_get_node_json(el_val_t id) { return engram_get_node_json(id); }
|
el_val_t __engram_get_node_json(el_val_t id) { return engram_get_node_json(id); }
|
||||||
el_val_t __engram_get_node_by_label(el_val_t label) { return engram_get_node_by_label(label); }
|
|
||||||
|
|
||||||
el_val_t __engram_search_json(el_val_t query, el_val_t limit) {
|
el_val_t __engram_search_json(el_val_t query, el_val_t limit) {
|
||||||
return engram_search_json(query, limit);
|
return engram_search_json(query, limit);
|
||||||
|
|||||||
@@ -226,7 +226,6 @@ el_val_t __engram_activate(el_val_t query, el_val_t depth);
|
|||||||
el_val_t __engram_save(el_val_t path);
|
el_val_t __engram_save(el_val_t path);
|
||||||
el_val_t __engram_load(el_val_t path);
|
el_val_t __engram_load(el_val_t path);
|
||||||
el_val_t __engram_get_node_json(el_val_t id);
|
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_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_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_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
|
||||||
|
|||||||
@@ -2670,7 +2670,6 @@ fn builtin_arity(name: String) -> Int {
|
|||||||
if str_eq(name, "engram_save") { return 1 }
|
if str_eq(name, "engram_save") { return 1 }
|
||||||
if str_eq(name, "engram_load") { return 1 }
|
if str_eq(name, "engram_load") { return 1 }
|
||||||
if str_eq(name, "engram_get_node_json") { return 1 }
|
if str_eq(name, "engram_get_node_json") { return 1 }
|
||||||
if str_eq(name, "engram_get_node_by_label") { return 1 }
|
|
||||||
if str_eq(name, "engram_search_json") { return 2 }
|
if str_eq(name, "engram_search_json") { return 2 }
|
||||||
if str_eq(name, "engram_scan_nodes_json") { return 2 }
|
if str_eq(name, "engram_scan_nodes_json") { return 2 }
|
||||||
if str_eq(name, "engram_neighbors_json") { return 3 }
|
if str_eq(name, "engram_neighbors_json") { return 3 }
|
||||||
|
|||||||
@@ -23,10 +23,29 @@ fn tok_at(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn tok_kind(tokens: [Any], pos: Int) -> String {
|
fn tok_kind(tokens: [Any], pos: Int) -> String {
|
||||||
|
// Out-of-range reads must report the Eof sentinel so every `== "Eof"`
|
||||||
|
// termination guard in the parser fires. Without this, reading past the
|
||||||
|
// single trailing Eof token returns runtime null (el_list_get OOB -> 0),
|
||||||
|
// which matches no delimiter, letting inner parse loops append AST nodes
|
||||||
|
// forever on malformed input -> unbounded allocation -> OOM.
|
||||||
|
let n: Int = native_list_len(tokens) / 2
|
||||||
|
if pos < 0 {
|
||||||
|
return "Eof"
|
||||||
|
}
|
||||||
|
if pos >= n {
|
||||||
|
return "Eof"
|
||||||
|
}
|
||||||
native_list_get(tokens, pos * 2)
|
native_list_get(tokens, pos * 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tok_value(tokens: [Any], pos: Int) -> String {
|
fn tok_value(tokens: [Any], pos: Int) -> String {
|
||||||
|
let n: Int = native_list_len(tokens) / 2
|
||||||
|
if pos < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if pos >= n {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
native_list_get(tokens, pos * 2 + 1)
|
native_list_get(tokens, pos * 2 + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +54,12 @@ fn expect(tokens: [Any], pos: Int, kind: String) -> Int {
|
|||||||
if k == kind {
|
if k == kind {
|
||||||
return pos + 1
|
return pos + 1
|
||||||
}
|
}
|
||||||
// On mismatch just advance; error recovery is best-effort
|
// On mismatch, error recovery is best-effort. But never step PAST the Eof
|
||||||
|
// sentinel: once at Eof a mismatch means the input ended early, and
|
||||||
|
// advancing would run the cursor off the token list.
|
||||||
|
if k == "Eof" {
|
||||||
|
return pos
|
||||||
|
}
|
||||||
pos + 1
|
pos + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user