fa2b49365b
jp_parse_string_raw handled \uXXXX by skipping the four hex digits and emitting a literal '?'. JSON writers escape non-ASCII by default (Python's json.dumps ships ensure_ascii=True; MCP clients do the same), so every em dash, curly quote, accented letter and emoji arriving over MCP or HTTP was silently replaced by one question mark on the way in. Measured on the live store: 3,119 of 4,081 non-telemetry nodes carried the damage, including the self traversal root and all 13 values nodes. Contents split cleanly into fully-clean or fully-mangled with zero overlap, which is the tell that it was one write path rather than gradual rot. No snapshot on disk predates it, and 3 bytes collapsing to 1 is not invertible, so the existing damage is permanent; only the forward path could be fixed. Decode properly instead: 4 hex digits, surrogate-pair reassembly for astral codepoints, U+FFFD for lone surrogates, UTF-8 encode. Malformed escapes keep the old '?' so a truncated body still parses. The deeper failure was that nothing measured this for two months. Every gauge in the system reports whether the machinery is running; none reported whether the text it carries is intact. Adds both halves: engram_text_health_json() / GET /api/text-health for the daily census, and a txt_damaged counter on the heartbeat for live regression. Verified in both directions - clean UTF-8 does not trip it, a deliberately damaged node does.
559 lines
27 KiB
C
559 lines
27 KiB
C
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include "el_runtime.h"
|
|
|
|
el_val_t parse_port(el_val_t bind);
|
|
el_val_t ok_json(void);
|
|
el_val_t err_json(el_val_t msg);
|
|
el_val_t strip_query(el_val_t path);
|
|
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 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_act_stats(el_val_t method, el_val_t path, el_val_t body);
|
|
el_val_t route_text_health(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_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_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_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_edges_batch(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_forget(el_val_t method, el_val_t path, el_val_t body);
|
|
el_val_t route_save(el_val_t method, el_val_t path, el_val_t body);
|
|
el_val_t 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_embed_backfill(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 route_similarity(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 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 port;
|
|
el_val_t data_dir_raw;
|
|
el_val_t data_dir;
|
|
el_val_t snapshot_path;
|
|
el_val_t boot_snap;
|
|
|
|
el_val_t parse_port(el_val_t bind) {
|
|
el_val_t colon = str_index_of(bind, EL_STR(":"));
|
|
if (colon < 0) {
|
|
return str_to_int(bind);
|
|
}
|
|
el_val_t after = str_slice(bind, (colon + 1), str_len(bind));
|
|
return str_to_int(after);
|
|
return 0;
|
|
}
|
|
|
|
el_val_t ok_json(void) {
|
|
return EL_STR("{\"ok\":true}");
|
|
return 0;
|
|
}
|
|
|
|
el_val_t err_json(el_val_t msg) {
|
|
return el_str_concat(el_str_concat(EL_STR("{\"error\":\""), msg), EL_STR("\"}"));
|
|
return 0;
|
|
}
|
|
|
|
el_val_t strip_query(el_val_t path) {
|
|
el_val_t q = str_index_of(path, EL_STR("?"));
|
|
if (q < 0) {
|
|
return path;
|
|
}
|
|
return str_slice(path, 0, q);
|
|
return 0;
|
|
}
|
|
|
|
el_val_t query_param(el_val_t path, el_val_t key) {
|
|
el_val_t q = str_index_of(path, EL_STR("?"));
|
|
if (q < 0) {
|
|
return EL_STR("");
|
|
}
|
|
el_val_t qs = str_slice(path, (q + 1), str_len(path));
|
|
el_val_t needle = el_str_concat(key, EL_STR("="));
|
|
el_val_t pos = str_index_of(qs, needle);
|
|
if (pos < 0) {
|
|
return EL_STR("");
|
|
}
|
|
el_val_t after = str_slice(qs, (pos + str_len(needle)), str_len(qs));
|
|
el_val_t amp = str_index_of(after, EL_STR("&"));
|
|
if (amp < 0) {
|
|
return after;
|
|
}
|
|
return str_slice(after, 0, amp);
|
|
return 0;
|
|
}
|
|
|
|
el_val_t query_int(el_val_t path, el_val_t key, el_val_t default_val) {
|
|
el_val_t v = query_param(path, key);
|
|
if (str_eq(v, EL_STR(""))) {
|
|
return default_val;
|
|
}
|
|
return str_to_int(v);
|
|
return 0;
|
|
}
|
|
|
|
el_val_t extract_id(el_val_t path, el_val_t prefix) {
|
|
el_val_t clean = strip_query(path);
|
|
if (!str_starts_with(clean, prefix)) {
|
|
return EL_STR("");
|
|
}
|
|
el_val_t after = str_slice(clean, str_len(prefix), str_len(clean));
|
|
el_val_t slash = str_index_of(after, EL_STR("/"));
|
|
if (slash < 0) {
|
|
return after;
|
|
}
|
|
return str_slice(after, 0, slash);
|
|
return 0;
|
|
}
|
|
|
|
el_val_t route_stats(el_val_t method, el_val_t path, el_val_t body) {
|
|
return engram_stats_json();
|
|
return 0;
|
|
}
|
|
|
|
el_val_t route_act_stats(el_val_t method, el_val_t path, el_val_t body) {
|
|
return engram_act_stats_json();
|
|
return 0;
|
|
}
|
|
|
|
el_val_t route_text_health(el_val_t method, el_val_t path, el_val_t body) {
|
|
return engram_text_health_json();
|
|
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 content = json_get_string(body, EL_STR("content"));
|
|
el_val_t nt_raw = json_get_string(body, EL_STR("node_type"));
|
|
el_val_t node_type = ({ el_val_t _if_result_2 = 0; if (str_eq(nt_raw, EL_STR(""))) { _if_result_2 = (EL_STR("Memory")); } else { _if_result_2 = (nt_raw); } _if_result_2; });
|
|
el_val_t sal_present = json_get_raw(body, EL_STR("salience"));
|
|
el_val_t salience = ({ el_val_t _if_result_3 = 0; if (str_eq(sal_present, EL_STR(""))) { _if_result_3 = (el_from_float(0.5)); } else { _if_result_3 = (json_get_float(body, EL_STR("salience"))); } _if_result_3; });
|
|
el_val_t label_raw = json_get_string(body, EL_STR("label"));
|
|
el_val_t label = ({ el_val_t _if_result_4 = 0; if (str_eq(label_raw, EL_STR(""))) { _if_result_4 = (content); } else { _if_result_4 = (label_raw); } _if_result_4; });
|
|
el_val_t imp_present = json_get_raw(body, EL_STR("importance"));
|
|
el_val_t importance = ({ el_val_t _if_result_5 = 0; if (str_eq(imp_present, EL_STR(""))) { _if_result_5 = (el_from_float(0.5)); } else { _if_result_5 = (json_get_float(body, EL_STR("importance"))); } _if_result_5; });
|
|
el_val_t conf_present = json_get_raw(body, EL_STR("confidence"));
|
|
el_val_t confidence = ({ el_val_t _if_result_6 = 0; if (str_eq(conf_present, EL_STR(""))) { _if_result_6 = (el_from_float(1.0)); } else { _if_result_6 = (json_get_float(body, EL_STR("confidence"))); } _if_result_6; });
|
|
el_val_t tier_raw = json_get_string(body, EL_STR("tier"));
|
|
el_val_t tier = ({ el_val_t _if_result_7 = 0; if (str_eq(tier_raw, EL_STR(""))) { _if_result_7 = (EL_STR("Working")); } else { _if_result_7 = (tier_raw); } _if_result_7; });
|
|
el_val_t tags = json_get_string(body, EL_STR("tags"));
|
|
el_val_t id = engram_node_full(content, node_type, label, salience, importance, confidence, tier, tags);
|
|
el_val_t saved = persist_canonical();
|
|
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"id\":\""), id), EL_STR("\",\"content\":\"")), content), EL_STR("\",\"node_type\":\"")), node_type), EL_STR("\"}"));
|
|
return 0;
|
|
}
|
|
|
|
el_val_t route_get_node(el_val_t method, el_val_t path, el_val_t body) {
|
|
el_val_t id = extract_id(path, EL_STR("/api/nodes/"));
|
|
if (str_eq(id, EL_STR(""))) {
|
|
return err_json(EL_STR("missing id"));
|
|
}
|
|
return engram_get_node_json(id);
|
|
return 0;
|
|
}
|
|
|
|
el_val_t route_scan_nodes(el_val_t method, el_val_t path, el_val_t body) {
|
|
el_val_t limit = query_int(path, EL_STR("limit"), 50);
|
|
el_val_t offset = query_int(path, EL_STR("offset"), 0);
|
|
el_val_t nt = query_param(path, EL_STR("node_type"));
|
|
if (str_eq(nt, EL_STR(""))) {
|
|
return engram_scan_nodes_json(limit, offset);
|
|
}
|
|
return engram_scan_nodes_by_type_json(nt, limit, offset);
|
|
return 0;
|
|
}
|
|
|
|
el_val_t route_scan_edges(el_val_t method, el_val_t path, el_val_t body) {
|
|
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
|
|
el_val_t dir = ({ el_val_t _if_result_8 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_8 = (EL_STR("/tmp/engram")); } else { _if_result_8 = (dir_raw); } _if_result_8; });
|
|
el_val_t snap_path = el_str_concat(dir, EL_STR("/.scan-export.json"));
|
|
engram_save(snap_path);
|
|
el_val_t snap = fs_read(snap_path);
|
|
if (str_eq(snap, EL_STR(""))) {
|
|
return EL_STR("[]");
|
|
}
|
|
el_val_t edges = json_get_raw(snap, EL_STR("edges"));
|
|
if (str_eq(edges, EL_STR(""))) {
|
|
return EL_STR("[]");
|
|
}
|
|
return edges;
|
|
return 0;
|
|
}
|
|
|
|
el_val_t route_search(el_val_t method, el_val_t path, el_val_t body) {
|
|
el_val_t q = ({ el_val_t _if_result_9 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_9 = (query_param(path, EL_STR("q"))); } else { _if_result_9 = (json_get_string(body, EL_STR("query"))); } _if_result_9; });
|
|
el_val_t lim_url = query_int(path, EL_STR("limit"), 0);
|
|
el_val_t lim_body = json_get_int(body, EL_STR("limit"));
|
|
el_val_t lim_either = ({ el_val_t _if_result_10 = 0; if ((lim_url > 0)) { _if_result_10 = (lim_url); } else { _if_result_10 = (lim_body); } _if_result_10; });
|
|
el_val_t limit = ({ el_val_t _if_result_11 = 0; if ((lim_either > 0)) { _if_result_11 = (lim_either); } else { _if_result_11 = (20); } _if_result_11; });
|
|
return engram_search_json(q, limit);
|
|
return 0;
|
|
}
|
|
|
|
el_val_t route_activate(el_val_t method, el_val_t path, el_val_t body) {
|
|
el_val_t q = ({ el_val_t _if_result_12 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_12 = (query_param(path, EL_STR("q"))); } else { _if_result_12 = (json_get_string(body, EL_STR("query"))); } _if_result_12; });
|
|
if (str_eq(q, EL_STR(""))) {
|
|
return err_json(EL_STR("missing query"));
|
|
}
|
|
el_val_t d_raw = ({ el_val_t _if_result_13 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_13 = (query_int(path, EL_STR("depth"), 3)); } else { _if_result_13 = (json_get_int(body, EL_STR("depth"))); } _if_result_13; });
|
|
el_val_t depth = ({ el_val_t _if_result_14 = 0; if ((d_raw > 0)) { _if_result_14 = (d_raw); } else { _if_result_14 = (3); } _if_result_14; });
|
|
return el_str_concat(el_str_concat(EL_STR("{\"results\":"), engram_activate_json(q, depth)), EL_STR("}"));
|
|
return 0;
|
|
}
|
|
|
|
el_val_t route_create_edge(el_val_t method, el_val_t path, el_val_t body) {
|
|
el_val_t from_id = json_get_string(body, EL_STR("from_id"));
|
|
el_val_t to_id = json_get_string(body, EL_STR("to_id"));
|
|
el_val_t rel_raw = json_get_string(body, EL_STR("relation"));
|
|
el_val_t relation = ({ el_val_t _if_result_15 = 0; if (str_eq(rel_raw, EL_STR(""))) { _if_result_15 = (EL_STR("associates")); } else { _if_result_15 = (rel_raw); } _if_result_15; });
|
|
el_val_t w_present = json_get_raw(body, EL_STR("weight"));
|
|
el_val_t weight = ({ el_val_t _if_result_16 = 0; if (str_eq(w_present, EL_STR(""))) { _if_result_16 = (el_from_float(0.5)); } else { _if_result_16 = (json_get_float(body, EL_STR("weight"))); } _if_result_16; });
|
|
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 0;
|
|
}
|
|
|
|
el_val_t route_create_edges_batch(el_val_t method, el_val_t path, el_val_t body) {
|
|
el_val_t arr = json_get_raw(body, EL_STR("edges"));
|
|
if (str_eq(arr, EL_STR(""))) {
|
|
return err_json(EL_STR("missing edges array"));
|
|
}
|
|
el_val_t n = json_array_len(arr);
|
|
if (n == 0) {
|
|
return EL_STR("{\"ok\":true,\"accepted\":0,\"skipped\":0}");
|
|
}
|
|
el_val_t i = 0;
|
|
el_val_t accepted = 0;
|
|
el_val_t skipped = 0;
|
|
while (i < n) {
|
|
el_val_t item = json_array_get(arr, i);
|
|
el_val_t from_id = json_get_string(item, EL_STR("from_id"));
|
|
el_val_t to_id = json_get_string(item, EL_STR("to_id"));
|
|
if (str_eq(from_id, EL_STR("")) || str_eq(to_id, EL_STR(""))) {
|
|
skipped = (skipped + 1);
|
|
} else {
|
|
el_val_t rel_raw = json_get_string(item, EL_STR("relation"));
|
|
el_val_t relation = ({ el_val_t _if_result_17 = 0; if (str_eq(rel_raw, EL_STR(""))) { _if_result_17 = (EL_STR("associates")); } else { _if_result_17 = (rel_raw); } _if_result_17; });
|
|
el_val_t w_present = json_get_raw(item, EL_STR("weight"));
|
|
el_val_t weight = ({ el_val_t _if_result_18 = 0; if (str_eq(w_present, EL_STR(""))) { _if_result_18 = (el_from_float(0.5)); } else { _if_result_18 = (json_get_float(item, EL_STR("weight"))); } _if_result_18; });
|
|
engram_connect(from_id, to_id, weight, relation);
|
|
accepted = (accepted + 1);
|
|
}
|
|
i = (i + 1);
|
|
}
|
|
if (accepted > 0) {
|
|
el_val_t saved = persist_canonical();
|
|
}
|
|
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"accepted\":"), int_to_str(accepted)), EL_STR(",\"skipped\":")), int_to_str(skipped)), EL_STR("}"));
|
|
return 0;
|
|
}
|
|
|
|
el_val_t route_neighbors(el_val_t method, el_val_t path, el_val_t body) {
|
|
el_val_t id = extract_id(path, EL_STR("/api/neighbors/"));
|
|
if (str_eq(id, EL_STR(""))) {
|
|
return err_json(EL_STR("missing id"));
|
|
}
|
|
el_val_t depth = query_int(path, EL_STR("depth"), 1);
|
|
return engram_neighbors_json(id, depth, EL_STR("both"));
|
|
return 0;
|
|
}
|
|
|
|
el_val_t route_strengthen(el_val_t method, el_val_t path, el_val_t body) {
|
|
el_val_t id = json_get_string(body, EL_STR("node_id"));
|
|
if (str_eq(id, EL_STR(""))) {
|
|
return err_json(EL_STR("missing node_id"));
|
|
}
|
|
engram_strengthen(id);
|
|
el_val_t saved = persist_canonical();
|
|
return ok_json();
|
|
return 0;
|
|
}
|
|
|
|
el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body) {
|
|
el_val_t id = extract_id(path, EL_STR("/api/nodes/"));
|
|
if (str_eq(id, EL_STR(""))) {
|
|
return err_json(EL_STR("missing id"));
|
|
}
|
|
engram_forget(id);
|
|
el_val_t saved = persist_canonical();
|
|
return ok_json();
|
|
return 0;
|
|
}
|
|
|
|
el_val_t route_save(el_val_t method, el_val_t path, el_val_t body) {
|
|
el_val_t p_raw = json_get_string(body, EL_STR("path"));
|
|
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
|
|
el_val_t dir = ({ el_val_t _if_result_19 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_19 = (EL_STR("/tmp/engram")); } else { _if_result_19 = (dir_raw); } _if_result_19; });
|
|
el_val_t p = ({ el_val_t _if_result_20 = 0; if (str_eq(p_raw, EL_STR(""))) { _if_result_20 = (el_str_concat(dir, EL_STR("/snapshot.json"))); } else { _if_result_20 = (p_raw); } _if_result_20; });
|
|
engram_save(p);
|
|
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"path\":\""), p), EL_STR("\"}"));
|
|
return 0;
|
|
}
|
|
|
|
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_21 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_21 = (EL_STR("/tmp/engram")); } else { _if_result_21 = (dir_raw); } _if_result_21; });
|
|
el_val_t p = ({ el_val_t _if_result_22 = 0; if (str_eq(p_raw, EL_STR(""))) { _if_result_22 = (el_str_concat(dir, EL_STR("/snapshot.json"))); } else { _if_result_22 = (p_raw); } _if_result_22; });
|
|
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_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"status\":\"ok\",\"engine\":\"engram-runtime-native\",\"node_count\":"), int_to_str(engram_node_count())), EL_STR(",\"edge_count\":")), int_to_str(engram_edge_count())), EL_STR("}"));
|
|
return 0;
|
|
}
|
|
|
|
el_val_t route_embed_backfill(el_val_t method, el_val_t path, el_val_t body) {
|
|
el_val_t n = query_int(path, EL_STR("n"), 32);
|
|
el_val_t result = engram_embed_backfill(n);
|
|
el_val_t done = json_get_float(result, EL_STR("embedded"));
|
|
if (done > el_from_float(0.0)) {
|
|
el_val_t saved = persist_canonical();
|
|
}
|
|
return result;
|
|
return 0;
|
|
}
|
|
|
|
el_val_t route_sync(el_val_t method, el_val_t path, el_val_t body) {
|
|
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
|
|
el_val_t dir = ({ el_val_t _if_result_23 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_23 = (EL_STR("/tmp/engram")); } else { _if_result_23 = (dir_raw); } _if_result_23; });
|
|
el_val_t snap_path = el_str_concat(dir, EL_STR("/.sync-export.json"));
|
|
engram_save(snap_path);
|
|
el_val_t snap = fs_read(snap_path);
|
|
if (str_eq(snap, EL_STR(""))) {
|
|
return err_json(EL_STR("sync export failed: snapshot unreadable"));
|
|
}
|
|
return snap;
|
|
return 0;
|
|
}
|
|
|
|
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"));
|
|
if (str_eq(p, EL_STR(""))) {
|
|
return err_json(EL_STR("path is required"));
|
|
}
|
|
if (str_eq(fs_read(p), EL_STR(""))) {
|
|
return err_json(EL_STR("file missing or empty"));
|
|
}
|
|
el_val_t before_n = engram_node_count();
|
|
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;
|
|
}
|
|
|
|
el_val_t route_emit_ise(el_val_t method, el_val_t path, el_val_t body) {
|
|
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 sal = el_from_float(0.3);
|
|
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_24 = 0; if (str_eq(ret_raw, EL_STR(""))) { _if_result_24 = (172800000); } else { _if_result_24 = (str_to_int(ret_raw)); } _if_result_24; });
|
|
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;
|
|
}
|
|
|
|
el_val_t route_capture_knowledge(el_val_t method, el_val_t path, el_val_t body) {
|
|
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_25 = 0; if (str_eq(title, EL_STR(""))) { _if_result_25 = (str_slice(content, 0, 60)); } else { _if_result_25 = (title); } _if_result_25; });
|
|
el_val_t category_raw = json_get_string(body, EL_STR("category"));
|
|
el_val_t category = ({ el_val_t _if_result_26 = 0; if (str_eq(category_raw, EL_STR(""))) { _if_result_26 = (EL_STR("other")); } else { _if_result_26 = (category_raw); } _if_result_26; });
|
|
el_val_t ktier_raw = json_get_string(body, EL_STR("tier"));
|
|
el_val_t ktier = ({ el_val_t _if_result_27 = 0; if (str_eq(ktier_raw, EL_STR(""))) { _if_result_27 = (EL_STR("note")); } else { _if_result_27 = (ktier_raw); } _if_result_27; });
|
|
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_28 = 0; if (str_eq(tags_raw, EL_STR(""))) { _if_result_28 = (EL_STR("[]")); } else { _if_result_28 = (tags_raw); } _if_result_28; });
|
|
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_29 = 0; if (str_eq(head, EL_STR("["))) { _if_result_29 = (EL_STR("")); } else { _if_result_29 = (EL_STR(",")); } _if_result_29; });
|
|
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_30 = 0; if (str_eq(safe_proj, EL_STR(""))) { _if_result_30 = (EL_STR("")); } else { _if_result_30 = (el_str_concat(el_str_concat(EL_STR(",\"project:"), safe_proj), EL_STR("\""))); } _if_result_30; });
|
|
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;
|
|
}
|
|
|
|
el_val_t route_similarity(el_val_t method, el_val_t path, el_val_t body) {
|
|
el_val_t a = query_param(path, EL_STR("a"));
|
|
el_val_t b = query_param(path, EL_STR("b"));
|
|
if (str_eq(a, EL_STR(""))) {
|
|
return err_json(EL_STR("missing a"));
|
|
}
|
|
if (str_eq(b, EL_STR(""))) {
|
|
return err_json(EL_STR("missing b"));
|
|
}
|
|
el_val_t sim = engram_cosine_sim(a, b);
|
|
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"a\":\""), a), EL_STR("\",\"b\":\"")), b), EL_STR("\",\"cosine\":")), float_to_str(sim)), EL_STR("}"));
|
|
return 0;
|
|
}
|
|
|
|
el_val_t check_auth_ok(el_val_t method, el_val_t body) {
|
|
el_val_t key = env(EL_STR("ENGRAM_API_KEY"));
|
|
if (str_eq(key, EL_STR(""))) {
|
|
return 1;
|
|
}
|
|
if (str_eq(method, EL_STR("GET"))) {
|
|
return 1;
|
|
}
|
|
el_val_t provided = json_get_string(body, EL_STR("_auth"));
|
|
if (str_eq(provided, key)) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
return 0;
|
|
}
|
|
|
|
el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
|
|
el_val_t clean = strip_query(path);
|
|
if (str_eq(method, EL_STR("GET"))) {
|
|
if (str_eq(clean, EL_STR("/health")) || str_eq(clean, EL_STR("/"))) {
|
|
return route_health(method, path, body);
|
|
}
|
|
}
|
|
if (str_eq(method, EL_STR("POST")) && str_eq(clean, EL_STR("/api/neuron/state-events"))) {
|
|
return route_emit_ise(method, path, body);
|
|
}
|
|
if (!check_auth_ok(method, body)) {
|
|
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")))) {
|
|
return route_stats(method, path, body);
|
|
}
|
|
if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/act-stats")) || str_eq(clean, EL_STR("/act-stats")))) {
|
|
return route_act_stats(method, path, body);
|
|
}
|
|
if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/text-health")) || str_eq(clean, EL_STR("/text-health")))) {
|
|
return route_text_health(method, path, body);
|
|
}
|
|
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/nodes")) || str_eq(clean, EL_STR("/nodes")))) {
|
|
return route_create_node(method, path, body);
|
|
}
|
|
if (str_eq(method, EL_STR("GET")) && (((str_eq(clean, EL_STR("/api/nodes")) || str_eq(clean, EL_STR("/nodes"))) || str_eq(clean, EL_STR("/nodes/list"))) || str_eq(clean, EL_STR("/api/nodes/list")))) {
|
|
return route_scan_nodes(method, path, body);
|
|
}
|
|
if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/edges")) || str_eq(clean, EL_STR("/edges")))) {
|
|
return route_scan_edges(method, path, body);
|
|
}
|
|
if (str_eq(method, EL_STR("GET")) && str_starts_with(clean, EL_STR("/api/nodes/"))) {
|
|
return route_get_node(method, path, body);
|
|
}
|
|
if (str_eq(method, EL_STR("DELETE")) && str_starts_with(clean, EL_STR("/api/nodes/"))) {
|
|
return route_forget(method, path, body);
|
|
}
|
|
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/edges")) || str_eq(clean, EL_STR("/edges")))) {
|
|
return route_create_edge(method, path, body);
|
|
}
|
|
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/edges/batch")) || str_eq(clean, EL_STR("/edges/batch")))) {
|
|
return route_create_edges_batch(method, path, body);
|
|
}
|
|
if (str_eq(method, EL_STR("GET")) && str_starts_with(clean, EL_STR("/api/neighbors/"))) {
|
|
return route_neighbors(method, path, body);
|
|
}
|
|
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/activate")) || str_eq(clean, EL_STR("/activate")))) {
|
|
return route_activate(method, path, body);
|
|
}
|
|
if (str_eq(method, EL_STR("GET")) && str_starts_with(clean, EL_STR("/api/activate"))) {
|
|
return route_activate(method, path, body);
|
|
}
|
|
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/search")) || str_eq(clean, EL_STR("/search")))) {
|
|
return route_search(method, path, body);
|
|
}
|
|
if (str_eq(method, EL_STR("GET")) && str_starts_with(clean, EL_STR("/api/search"))) {
|
|
return route_search(method, path, body);
|
|
}
|
|
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);
|
|
}
|
|
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);
|
|
}
|
|
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);
|
|
}
|
|
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);
|
|
}
|
|
if (str_eq(clean, EL_STR("/api/embed-backfill"))) {
|
|
return route_embed_backfill(method, path, body);
|
|
}
|
|
if (str_eq(method, EL_STR("GET")) && str_starts_with(clean, EL_STR("/api/similarity"))) {
|
|
return route_similarity(method, path, body);
|
|
}
|
|
return el_str_concat(el_str_concat(EL_STR("{\"error\":\"not found\",\"path\":\""), clean), EL_STR("\"}"));
|
|
return 0;
|
|
}
|
|
|
|
int main(int _argc, char** _argv) {
|
|
el_runtime_init_args(_argc, _argv);
|
|
bind_raw = env(EL_STR("ENGRAM_BIND"));
|
|
bind_str = ({ el_val_t _if_result_31 = 0; if (str_eq(bind_raw, EL_STR(""))) { _if_result_31 = (EL_STR(":8742")); } else { _if_result_31 = (bind_raw); } _if_result_31; });
|
|
port = parse_port(bind_str);
|
|
data_dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
|
|
data_dir = ({ el_val_t _if_result_32 = 0; if (str_eq(data_dir_raw, EL_STR(""))) { _if_result_32 = (EL_STR("/tmp/engram")); } else { _if_result_32 = (data_dir_raw); } _if_result_32; });
|
|
snapshot_path = el_str_concat(data_dir, EL_STR("/snapshot.json"));
|
|
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_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] edge_count="), int_to_str(engram_edge_count())));
|
|
println(el_str_concat(EL_STR("[engram] listening on "), int_to_str(port)));
|
|
http_set_handler(EL_STR("handle_request"));
|
|
http_serve(port, EL_STR("handle_request"));
|
|
return 0;
|
|
}
|
|
|