Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a89080e3d4 |
Vendored
BIN
Binary file not shown.
Vendored
+2
-38
@@ -20,8 +20,6 @@ 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);
|
||||||
@@ -117,7 +115,7 @@ el_val_t route_create_node(el_val_t method, el_val_t path, el_val_t body) {
|
|||||||
node_type = EL_STR("Memory");
|
node_type = EL_STR("Memory");
|
||||||
}
|
}
|
||||||
el_val_t salience = json_get_float(body, EL_STR("salience"));
|
el_val_t salience = json_get_float(body, EL_STR("salience"));
|
||||||
if (salience == el_from_float(0.0)) {
|
if (str_eq(salience, el_from_float(0.0))) {
|
||||||
salience = el_from_float(0.5);
|
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);
|
||||||
@@ -207,7 +205,7 @@ el_val_t route_create_edge(el_val_t method, el_val_t path, el_val_t body) {
|
|||||||
relation = EL_STR("associates");
|
relation = EL_STR("associates");
|
||||||
}
|
}
|
||||||
el_val_t weight = json_get_float(body, EL_STR("weight"));
|
el_val_t weight = json_get_float(body, EL_STR("weight"));
|
||||||
if (weight == el_from_float(0.0)) {
|
if (str_eq(weight, el_from_float(0.0))) {
|
||||||
weight = el_from_float(0.5);
|
weight = el_from_float(0.5);
|
||||||
}
|
}
|
||||||
engram_connect(from_id, to_id, weight, relation);
|
engram_connect(from_id, to_id, weight, relation);
|
||||||
@@ -245,34 +243,6 @@ el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
el_val_t route_create_ise(el_val_t method, el_val_t path, el_val_t body) {
|
|
||||||
el_val_t 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\"]"));
|
|
||||||
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), id), EL_STR("\"}"));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
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"));
|
|
||||||
if (str_eq(dir, EL_STR(""))) {
|
|
||||||
dir = EL_STR("/tmp/engram");
|
|
||||||
}
|
|
||||||
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 EL_STR("{\"nodes\":[],\"edges\":[]}");
|
|
||||||
}
|
|
||||||
return snap;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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(""))) {
|
||||||
@@ -329,9 +299,6 @@ 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"))) {
|
|
||||||
return route_create_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"));
|
||||||
}
|
}
|
||||||
@@ -374,9 +341,6 @@ 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,43 +180,6 @@ fn route_forget(method: String, path: String, body: String) -> String {
|
|||||||
ok_json()
|
ok_json()
|
||||||
}
|
}
|
||||||
|
|
||||||
// route_create_ise — POST /api/neuron/state-events
|
|
||||||
// Creates an InternalStateEvent node from a JSON body with a "content" field.
|
|
||||||
// Returns {"ok":true,"id":"<uuid>"}. Used by the soul daemon's ise_post() to
|
|
||||||
// record internal state transitions in the authoritative Engram store.
|
|
||||||
// This route was in the original server.el but was lost during a refactor;
|
|
||||||
// its absence would break ISE recording on the next Engram restart.
|
|
||||||
// (Restored 2026-06-30 self-review)
|
|
||||||
// importance=0.3 matches awareness.el in-process fallback (engram_node_full ISE
|
|
||||||
// defaults). Original had 0.5 which was a mismatch. (Corrected 2026-06-30)
|
|
||||||
fn route_create_ise(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 sal: Float = 0.3
|
|
||||||
let imp: Float = 0.3
|
|
||||||
let conf: Float = 0.8
|
|
||||||
let id: String = engram_node_full(content, "InternalStateEvent", "state-event",
|
|
||||||
sal, imp, conf, "Episodic", "[\"internal-state\",\"InternalStateEvent\"]")
|
|
||||||
"{\"ok\":true,\"id\":\"" + id + "\"}"
|
|
||||||
}
|
|
||||||
|
|
||||||
// route_sync — GET /api/sync
|
|
||||||
// Returns the full graph snapshot as JSON (nodes + edges), suitable for loading
|
|
||||||
// via engram_load_merge. Used by the soul daemon's periodic refresh cycle to
|
|
||||||
// keep its in-process Engram store in sync with this authoritative HTTP store.
|
|
||||||
// Saves a temporary snapshot to avoid holding a large in-memory string while
|
|
||||||
// streaming — caller reads the file through the HTTP response body.
|
|
||||||
// (Restored 2026-06-30 self-review)
|
|
||||||
fn route_sync(method: String, path: String, body: String) -> String {
|
|
||||||
let dir: String = env("ENGRAM_DATA_DIR")
|
|
||||||
if str_eq(dir, "") { let dir = "/tmp/engram" }
|
|
||||||
let snap_path: String = dir + "/sync-export.json"
|
|
||||||
engram_save(snap_path)
|
|
||||||
let snap: String = fs_read(snap_path)
|
|
||||||
if str_eq(snap, "") { return "{\"nodes\":[],\"edges\":[]}" }
|
|
||||||
snap
|
|
||||||
}
|
|
||||||
|
|
||||||
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: String = json_get_string(body, "path")
|
||||||
if str_eq(p, "") {
|
if str_eq(p, "") {
|
||||||
@@ -269,14 +232,6 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal state events bypass auth — only the local soul daemon calls
|
|
||||||
// this route, and ise_post() does not include an _auth field in its body.
|
|
||||||
// Placing this before the auth gate preserves the old binary's behavior
|
|
||||||
// and ensures the soul daemon can always write ISEs.
|
|
||||||
if str_eq(method, "POST") && str_starts_with(clean, "/api/neuron/state-events") {
|
|
||||||
return route_create_ise(method, path, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auth (when ENGRAM_API_KEY is set)
|
// Auth (when ENGRAM_API_KEY is set)
|
||||||
if !check_auth_ok(method, body) {
|
if !check_auth_ok(method, body) {
|
||||||
return err_json("unauthorized")
|
return err_json("unauthorized")
|
||||||
@@ -331,11 +286,6 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
|||||||
return route_strengthen(method, path, body)
|
return route_strengthen(method, path, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync — soul daemon fetches here for periodic in-process graph refresh
|
|
||||||
if str_eq(method, "GET") && (str_eq(clean, "/api/sync") || str_eq(clean, "/sync")) {
|
|
||||||
return route_sync(method, path, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Persistence
|
// Persistence
|
||||||
if str_eq(method, "POST") && (str_eq(clean, "/api/save") || str_eq(clean, "/save")) {
|
if str_eq(method, "POST") && (str_eq(clean, "/api/save") || str_eq(clean, "/save")) {
|
||||||
return route_save(method, path, body)
|
return route_save(method, path, body)
|
||||||
|
|||||||
@@ -1632,6 +1632,83 @@ el_val_t http_serve(el_val_t port, el_val_t handler) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── http_serve_async — non-blocking HTTP server ─────────────────────────── */
|
||||||
|
/* Runs the accept loop in a background pthread, returns immediately so the
|
||||||
|
* calling EL script can continue (e.g. to run an awareness loop).
|
||||||
|
*
|
||||||
|
* El signature: http_serve_async(port, handler) -> Void */
|
||||||
|
|
||||||
|
typedef struct { int sock; } HttpServeAsyncArg;
|
||||||
|
|
||||||
|
static void* _http_serve_async_loop(void* raw) {
|
||||||
|
HttpServeAsyncArg* a = (HttpServeAsyncArg*)raw;
|
||||||
|
int sock = a->sock;
|
||||||
|
free(a);
|
||||||
|
while (1) {
|
||||||
|
struct sockaddr_in6 cli;
|
||||||
|
socklen_t clen = sizeof(cli);
|
||||||
|
int cfd = accept(sock, (struct sockaddr*)&cli, &clen);
|
||||||
|
if (cfd < 0) {
|
||||||
|
if (errno == EINTR) continue;
|
||||||
|
perror("accept"); break;
|
||||||
|
}
|
||||||
|
pthread_mutex_lock(&_http_conn_mu);
|
||||||
|
while (_http_conn_active >= HTTP_MAX_CONNS) {
|
||||||
|
pthread_cond_wait(&_http_conn_cv, &_http_conn_mu);
|
||||||
|
}
|
||||||
|
_http_conn_active++;
|
||||||
|
pthread_mutex_unlock(&_http_conn_mu);
|
||||||
|
HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg));
|
||||||
|
if (!arg) { close(cfd); continue; }
|
||||||
|
arg->fd = cfd;
|
||||||
|
pthread_t tid;
|
||||||
|
if (pthread_create(&tid, NULL, http_worker, arg) != 0) {
|
||||||
|
close(cfd); free(arg);
|
||||||
|
pthread_mutex_lock(&_http_conn_mu);
|
||||||
|
_http_conn_active--;
|
||||||
|
pthread_cond_signal(&_http_conn_cv);
|
||||||
|
pthread_mutex_unlock(&_http_conn_mu);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pthread_detach(tid);
|
||||||
|
}
|
||||||
|
close(sock);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void http_serve_async(el_val_t port, el_val_t handler) {
|
||||||
|
const char* hname = EL_CSTR(handler);
|
||||||
|
if (hname && looks_like_string(handler)) {
|
||||||
|
http_set_handler(handler);
|
||||||
|
}
|
||||||
|
int p = (int)port;
|
||||||
|
if (p <= 0 || p > 65535) { fprintf(stderr, "http_serve_async: invalid port %d\n", p); return; }
|
||||||
|
int sock = socket(AF_INET6, SOCK_STREAM, 0);
|
||||||
|
if (sock < 0) { perror("socket"); return; }
|
||||||
|
int yes = 1; int no = 0;
|
||||||
|
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
||||||
|
setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no));
|
||||||
|
struct sockaddr_in6 addr;
|
||||||
|
memset(&addr, 0, sizeof(addr));
|
||||||
|
addr.sin6_family = AF_INET6;
|
||||||
|
addr.sin6_addr = in6addr_any;
|
||||||
|
addr.sin6_port = htons((uint16_t)p);
|
||||||
|
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||||
|
perror("bind"); close(sock); return;
|
||||||
|
}
|
||||||
|
if (listen(sock, 64) < 0) { perror("listen"); close(sock); return; }
|
||||||
|
fprintf(stderr, "[http] async listening on [::]:%d (dual-stack)\n", p);
|
||||||
|
HttpServeAsyncArg* a = malloc(sizeof(HttpServeAsyncArg));
|
||||||
|
if (!a) { close(sock); return; }
|
||||||
|
a->sock = sock;
|
||||||
|
pthread_t tid;
|
||||||
|
if (pthread_create(&tid, NULL, _http_serve_async_loop, a) != 0) {
|
||||||
|
perror("pthread_create"); free(a); close(sock); return;
|
||||||
|
}
|
||||||
|
pthread_detach(tid);
|
||||||
|
/* Returns immediately — caller can now run awareness_run() or any loop. */
|
||||||
|
}
|
||||||
|
|
||||||
/* ── HTTP server v2 — request headers + structured response ──────────────── */
|
/* ── HTTP server v2 — request headers + structured response ──────────────── */
|
||||||
/*
|
/*
|
||||||
* v2 widens the handler signature from
|
* v2 widens the handler signature from
|
||||||
@@ -1882,83 +1959,6 @@ el_val_t http_serve_v2(el_val_t port, el_val_t handler) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── http_serve_async — non-blocking HTTP server ─────────────────────────── */
|
|
||||||
/* Runs the accept loop in a background pthread, returns immediately so the
|
|
||||||
* calling EL script can continue (e.g. to run an awareness loop).
|
|
||||||
*
|
|
||||||
* El signature: http_serve_async(port, handler) -> Void */
|
|
||||||
|
|
||||||
typedef struct { int sock; } HttpServeAsyncArg;
|
|
||||||
|
|
||||||
static void* _http_serve_async_loop(void* raw) {
|
|
||||||
HttpServeAsyncArg* a = (HttpServeAsyncArg*)raw;
|
|
||||||
int sock = a->sock;
|
|
||||||
free(a);
|
|
||||||
while (1) {
|
|
||||||
struct sockaddr_in6 cli;
|
|
||||||
socklen_t clen = sizeof(cli);
|
|
||||||
int cfd = accept(sock, (struct sockaddr*)&cli, &clen);
|
|
||||||
if (cfd < 0) {
|
|
||||||
if (errno == EINTR) continue;
|
|
||||||
perror("accept"); break;
|
|
||||||
}
|
|
||||||
pthread_mutex_lock(&_http_conn_mu);
|
|
||||||
while (_http_conn_active >= HTTP_MAX_CONNS) {
|
|
||||||
pthread_cond_wait(&_http_conn_cv, &_http_conn_mu);
|
|
||||||
}
|
|
||||||
_http_conn_active++;
|
|
||||||
pthread_mutex_unlock(&_http_conn_mu);
|
|
||||||
HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg));
|
|
||||||
if (!arg) { close(cfd); continue; }
|
|
||||||
arg->fd = cfd;
|
|
||||||
pthread_t tid;
|
|
||||||
if (pthread_create(&tid, NULL, http_worker, arg) != 0) {
|
|
||||||
close(cfd); free(arg);
|
|
||||||
pthread_mutex_lock(&_http_conn_mu);
|
|
||||||
_http_conn_active--;
|
|
||||||
pthread_cond_signal(&_http_conn_cv);
|
|
||||||
pthread_mutex_unlock(&_http_conn_mu);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
pthread_detach(tid);
|
|
||||||
}
|
|
||||||
close(sock);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
void http_serve_async(el_val_t port, el_val_t handler) {
|
|
||||||
const char* hname = EL_CSTR(handler);
|
|
||||||
if (hname && looks_like_string(handler)) {
|
|
||||||
http_set_handler(handler);
|
|
||||||
}
|
|
||||||
int p = (int)port;
|
|
||||||
if (p <= 0 || p > 65535) { fprintf(stderr, "http_serve_async: invalid port %d\n", p); return; }
|
|
||||||
int sock = socket(AF_INET6, SOCK_STREAM, 0);
|
|
||||||
if (sock < 0) { perror("socket"); return; }
|
|
||||||
int yes = 1; int no = 0;
|
|
||||||
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
|
||||||
setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no));
|
|
||||||
struct sockaddr_in6 addr;
|
|
||||||
memset(&addr, 0, sizeof(addr));
|
|
||||||
addr.sin6_family = AF_INET6;
|
|
||||||
addr.sin6_addr = in6addr_any;
|
|
||||||
addr.sin6_port = htons((uint16_t)p);
|
|
||||||
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
|
||||||
perror("bind"); close(sock); return;
|
|
||||||
}
|
|
||||||
if (listen(sock, 64) < 0) { perror("listen"); close(sock); return; }
|
|
||||||
fprintf(stderr, "[http] async listening on [::]:%d (dual-stack)\n", p);
|
|
||||||
HttpServeAsyncArg* a = malloc(sizeof(HttpServeAsyncArg));
|
|
||||||
if (!a) { close(sock); return; }
|
|
||||||
a->sock = sock;
|
|
||||||
pthread_t tid;
|
|
||||||
if (pthread_create(&tid, NULL, _http_serve_async_loop, a) != 0) {
|
|
||||||
perror("pthread_create"); free(a); close(sock); return;
|
|
||||||
}
|
|
||||||
pthread_detach(tid);
|
|
||||||
/* Returns immediately — caller can now run awareness_run() or any loop. */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Build the response envelope a 4-arg handler can return. We hand-write
|
/* Build the response envelope a 4-arg handler can return. We hand-write
|
||||||
* the JSON so the discriminator key always lands first — the runtime's
|
* the JSON so the discriminator key always lands first — the runtime's
|
||||||
* http_parse_envelope() detects it via prefix match. headers_json must be
|
* http_parse_envelope() detects it via prefix match. headers_json must be
|
||||||
@@ -7585,6 +7585,159 @@ el_val_t engram_load(el_val_t path) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* engram_load_merge — like engram_load but WITHOUT resetting the store.
|
||||||
|
* Reads a JSON snapshot from `path` and adds any nodes/edges not already
|
||||||
|
* present in the in-memory graph. Dedup is by node id (for nodes) and by
|
||||||
|
* (from_id, to_id, relation) tuple (for edges).
|
||||||
|
*
|
||||||
|
* Returns (as an EL int) the count of new nodes added. Embeddings are
|
||||||
|
* intentionally skipped on merged nodes to avoid Ollama delays at runtime;
|
||||||
|
* auto_link_semantic will handle them when nodes are next activated.
|
||||||
|
*
|
||||||
|
* Does not merge layers — the in-process layer registry is authoritative. */
|
||||||
|
el_val_t engram_load_merge(el_val_t path) {
|
||||||
|
const char* p = EL_CSTR(path);
|
||||||
|
if (!p || !*p) return 0;
|
||||||
|
FILE* f = fopen(p, "rb");
|
||||||
|
if (!f) return 0;
|
||||||
|
fseek(f, 0, SEEK_END);
|
||||||
|
long sz = ftell(f);
|
||||||
|
rewind(f);
|
||||||
|
if (sz <= 0) { fclose(f); return 0; }
|
||||||
|
char* data = malloc((size_t)sz + 1);
|
||||||
|
if (!data) { fclose(f); return 0; }
|
||||||
|
size_t got = fread(data, 1, (size_t)sz, f);
|
||||||
|
fclose(f);
|
||||||
|
data[got] = '\0';
|
||||||
|
|
||||||
|
EngramStore* g = engram_get();
|
||||||
|
int64_t added_nodes = 0;
|
||||||
|
|
||||||
|
/* Walk nodes array — skip any node whose id already exists */
|
||||||
|
const char* nodes_p = json_find_key(data, "nodes");
|
||||||
|
if (nodes_p) {
|
||||||
|
nodes_p = eg_skip_ws(nodes_p);
|
||||||
|
if (*nodes_p == '[') {
|
||||||
|
nodes_p++;
|
||||||
|
nodes_p = eg_skip_ws(nodes_p);
|
||||||
|
while (*nodes_p && *nodes_p != ']') {
|
||||||
|
if (*nodes_p != '{') { nodes_p++; continue; }
|
||||||
|
const char* end = json_skip_value(nodes_p);
|
||||||
|
size_t n = (size_t)(end - nodes_p);
|
||||||
|
char* obj = malloc(n + 1);
|
||||||
|
memcpy(obj, nodes_p, n); obj[n] = '\0';
|
||||||
|
char* nid = eg_get_str_field(obj, "id");
|
||||||
|
int already = (nid && *nid && engram_find_node(nid) != NULL);
|
||||||
|
free(nid);
|
||||||
|
if (!already) {
|
||||||
|
engram_grow_nodes();
|
||||||
|
EngramNode* nn = &g->nodes[g->node_count];
|
||||||
|
memset(nn, 0, sizeof(*nn));
|
||||||
|
nn->id = eg_get_str_field(obj, "id");
|
||||||
|
nn->content = eg_get_str_field(obj, "content");
|
||||||
|
nn->node_type = eg_get_str_field(obj, "node_type");
|
||||||
|
nn->label = eg_get_str_field(obj, "label");
|
||||||
|
nn->tier = eg_get_str_field(obj, "tier");
|
||||||
|
nn->tags = eg_get_str_field(obj, "tags");
|
||||||
|
nn->metadata = eg_get_str_field(obj, "metadata");
|
||||||
|
if (!nn->metadata || !*nn->metadata) { free(nn->metadata); nn->metadata = strdup("{}"); }
|
||||||
|
nn->salience = eg_get_num_field(obj, "salience");
|
||||||
|
nn->importance = eg_get_num_field(obj, "importance");
|
||||||
|
nn->confidence = eg_get_num_field(obj, "confidence");
|
||||||
|
nn->temporal_decay_rate = eg_get_num_field(obj, "temporal_decay_rate");
|
||||||
|
nn->activation_count = eg_get_int_field(obj, "activation_count");
|
||||||
|
nn->last_activated = eg_get_int_field(obj, "last_activated");
|
||||||
|
nn->created_at = eg_get_int_field(obj, "created_at");
|
||||||
|
nn->updated_at = eg_get_int_field(obj, "updated_at");
|
||||||
|
nn->background_activation = eg_get_num_field(obj, "background_activation");
|
||||||
|
nn->working_memory_weight = eg_get_num_field(obj, "working_memory_weight");
|
||||||
|
if (!isfinite(nn->working_memory_weight) || nn->working_memory_weight < 0.0 || nn->working_memory_weight > 1.0)
|
||||||
|
nn->working_memory_weight = 0.0; /* clamp corrupt snapshot values */
|
||||||
|
nn->suppression_count = (int32_t)eg_get_int_field(obj, "suppression_count");
|
||||||
|
if (json_find_key(obj, "layer_id")) {
|
||||||
|
nn->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id");
|
||||||
|
} else {
|
||||||
|
nn->layer_id = ENGRAM_LAYER_DEFAULT;
|
||||||
|
}
|
||||||
|
g->node_count++;
|
||||||
|
added_nodes++;
|
||||||
|
}
|
||||||
|
free(obj);
|
||||||
|
nodes_p = end;
|
||||||
|
nodes_p = eg_skip_ws(nodes_p);
|
||||||
|
if (*nodes_p == ',') { nodes_p++; nodes_p = eg_skip_ws(nodes_p); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Walk edges array — skip if (from_id, to_id, relation) already present */
|
||||||
|
const char* edges_p = json_find_key(data, "edges");
|
||||||
|
if (edges_p) {
|
||||||
|
edges_p = eg_skip_ws(edges_p);
|
||||||
|
if (*edges_p == '[') {
|
||||||
|
edges_p++;
|
||||||
|
edges_p = eg_skip_ws(edges_p);
|
||||||
|
while (*edges_p && *edges_p != ']') {
|
||||||
|
if (*edges_p != '{') { edges_p++; continue; }
|
||||||
|
const char* end = json_skip_value(edges_p);
|
||||||
|
size_t n = (size_t)(end - edges_p);
|
||||||
|
char* obj = malloc(n + 1);
|
||||||
|
memcpy(obj, edges_p, n); obj[n] = '\0';
|
||||||
|
char* efrom = eg_get_str_field(obj, "from_id");
|
||||||
|
char* eto = eg_get_str_field(obj, "to_id");
|
||||||
|
char* erel = eg_get_str_field(obj, "relation");
|
||||||
|
/* Check for duplicate by scanning existing edges */
|
||||||
|
int dup = 0;
|
||||||
|
if (efrom && eto && erel) {
|
||||||
|
for (int64_t ei = 0; ei < g->edge_count; ei++) {
|
||||||
|
EngramEdge* ex = &g->edges[ei];
|
||||||
|
if (ex->from_id && ex->to_id && ex->relation &&
|
||||||
|
strcmp(ex->from_id, efrom) == 0 &&
|
||||||
|
strcmp(ex->to_id, eto) == 0 &&
|
||||||
|
strcmp(ex->relation, erel) == 0) {
|
||||||
|
dup = 1; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!dup) {
|
||||||
|
engram_grow_edges();
|
||||||
|
EngramEdge* ee = &g->edges[g->edge_count];
|
||||||
|
memset(ee, 0, sizeof(*ee));
|
||||||
|
ee->id = eg_get_str_field(obj, "id");
|
||||||
|
ee->from_id = efrom ? efrom : strdup("");
|
||||||
|
ee->to_id = eto ? eto : strdup("");
|
||||||
|
ee->relation = erel ? erel : strdup("");
|
||||||
|
ee->metadata = eg_get_str_field(obj, "metadata");
|
||||||
|
if (!ee->metadata || !*ee->metadata) { free(ee->metadata); ee->metadata = strdup("{}"); }
|
||||||
|
ee->weight = eg_get_num_field(obj, "weight");
|
||||||
|
ee->confidence = eg_get_num_field(obj, "confidence");
|
||||||
|
ee->created_at = eg_get_int_field(obj, "created_at");
|
||||||
|
ee->updated_at = eg_get_int_field(obj, "updated_at");
|
||||||
|
ee->last_fired = eg_get_int_field(obj, "last_fired");
|
||||||
|
ee->inhibitory = (int)eg_get_int_field(obj, "inhibitory");
|
||||||
|
if (json_find_key(obj, "layer_id")) {
|
||||||
|
ee->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id");
|
||||||
|
} else {
|
||||||
|
ee->layer_id = ENGRAM_LAYER_DEFAULT;
|
||||||
|
}
|
||||||
|
g->edge_count++;
|
||||||
|
/* NOTE: efrom/eto/erel ownership transferred to ee above */
|
||||||
|
efrom = NULL; eto = NULL; erel = NULL;
|
||||||
|
} else {
|
||||||
|
free(efrom); free(eto); free(erel);
|
||||||
|
}
|
||||||
|
free(obj);
|
||||||
|
edges_p = end;
|
||||||
|
edges_p = eg_skip_ws(edges_p);
|
||||||
|
if (*edges_p == ',') { edges_p++; edges_p = eg_skip_ws(edges_p); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free(data);
|
||||||
|
return (el_val_t)added_nodes;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Engram JSON-string accessors ─────────────────────────────────────────
|
/* ── Engram JSON-string accessors ─────────────────────────────────────────
|
||||||
* These return pre-serialized JSON strings so callers (especially HTTP
|
* These return pre-serialized JSON strings so callers (especially HTTP
|
||||||
* handlers) don't have to round-trip ElList/ElMap through json_stringify
|
* handlers) don't have to round-trip ElList/ElMap through json_stringify
|
||||||
@@ -7813,6 +7966,86 @@ el_val_t engram_stats_json(void) {
|
|||||||
return el_wrap_str(el_strdup(buf));
|
return el_wrap_str(el_strdup(buf));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Working memory accessors ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
el_val_t engram_wm_count(void) {
|
||||||
|
EngramStore* g = engram_get();
|
||||||
|
int64_t count = 0;
|
||||||
|
for (int64_t i = 0; i < g->node_count; i++) {
|
||||||
|
if (g->nodes[i].working_memory_weight > 0.0) count++;
|
||||||
|
}
|
||||||
|
return (el_val_t)count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Average working_memory_weight across all promoted nodes (wm > 0).
|
||||||
|
* Returns the float bit-pattern via el_from_float so EL can use it with
|
||||||
|
* float_to_str / float_gt. Returns 0.0 when no nodes are promoted. */
|
||||||
|
el_val_t engram_wm_avg_weight(void) {
|
||||||
|
EngramStore* g = engram_get();
|
||||||
|
double sum = 0.0;
|
||||||
|
int64_t count = 0;
|
||||||
|
for (int64_t i = 0; i < g->node_count; i++) {
|
||||||
|
double w = g->nodes[i].working_memory_weight;
|
||||||
|
if (w > 0.0 && w <= 1.0 && isfinite(w)) { sum += w; count++; }
|
||||||
|
}
|
||||||
|
double avg = (count > 0) ? (sum / (double)count) : 0.0;
|
||||||
|
return el_from_float(avg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* engram_wm_top_json — return top N working-memory nodes (by wm weight) as a
|
||||||
|
* compact JSON array for ISE heartbeat reporting.
|
||||||
|
* Each element: {"label":"...","node_type":"...","tier":"...","wm":0.42} */
|
||||||
|
el_val_t engram_wm_top_json(el_val_t n_v) {
|
||||||
|
int64_t top_n = (int64_t)n_v;
|
||||||
|
if (top_n <= 0) top_n = 10;
|
||||||
|
if (top_n > 50) top_n = 50;
|
||||||
|
EngramStore* g = engram_get();
|
||||||
|
|
||||||
|
int64_t* idx = malloc((size_t)(g->node_count + 1) * sizeof(int64_t));
|
||||||
|
if (!idx) return el_wrap_str(el_strdup("[]"));
|
||||||
|
int64_t mc = 0;
|
||||||
|
for (int64_t i = 0; i < g->node_count; i++) {
|
||||||
|
if (g->nodes[i].working_memory_weight > 0.0) {
|
||||||
|
const char* nt = g->nodes[i].node_type;
|
||||||
|
if (nt && strcmp(nt, "InternalStateEvent") == 0) continue;
|
||||||
|
idx[mc++] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Insertion-sort descending by wm weight (mc is typically small). */
|
||||||
|
for (int64_t i = 1; i < mc; i++) {
|
||||||
|
int64_t key = idx[i];
|
||||||
|
double kw = g->nodes[key].working_memory_weight;
|
||||||
|
int64_t j = i;
|
||||||
|
while (j > 0 && g->nodes[idx[j-1]].working_memory_weight < kw) {
|
||||||
|
idx[j] = idx[j-1]; j--;
|
||||||
|
}
|
||||||
|
idx[j] = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t emit = mc < top_n ? mc : top_n;
|
||||||
|
JsonBuf b; jb_init(&b);
|
||||||
|
jb_putc(&b, '[');
|
||||||
|
for (int64_t k = 0; k < emit; k++) {
|
||||||
|
EngramNode* n = &g->nodes[idx[k]];
|
||||||
|
if (k > 0) jb_putc(&b, ',');
|
||||||
|
jb_putc(&b, '{');
|
||||||
|
jb_puts(&b, "\"label\":");
|
||||||
|
jb_emit_escaped(&b, n->label ? n->label : "");
|
||||||
|
jb_puts(&b, ",\"node_type\":");
|
||||||
|
jb_emit_escaped(&b, n->node_type ? n->node_type : "");
|
||||||
|
jb_puts(&b, ",\"tier\":");
|
||||||
|
jb_emit_escaped(&b, n->tier ? n->tier : "");
|
||||||
|
char tmp[48];
|
||||||
|
snprintf(tmp, sizeof(tmp), ",\"wm\":%.3f", n->working_memory_weight);
|
||||||
|
jb_puts(&b, tmp);
|
||||||
|
jb_putc(&b, '}');
|
||||||
|
}
|
||||||
|
free(idx);
|
||||||
|
jb_putc(&b, ']');
|
||||||
|
return el_wrap_str(b.buf);
|
||||||
|
}
|
||||||
|
|
||||||
/* engram_list_layers_json — serialized counterpart of engram_list_layers.
|
/* engram_list_layers_json — serialized counterpart of engram_list_layers.
|
||||||
* Returns a JSON array, sorted by activation_priority ascending. */
|
* Returns a JSON array, sorted by activation_priority ascending. */
|
||||||
el_val_t engram_list_layers_json(void) {
|
el_val_t engram_list_layers_json(void) {
|
||||||
@@ -7992,257 +8225,6 @@ el_val_t engram_query_range(el_val_t start_ms_v, el_val_t end_ms_v) {
|
|||||||
return el_wrap_str(b.buf);
|
return el_wrap_str(b.buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* engram_load_merge — like engram_load but WITHOUT resetting the store.
|
|
||||||
* Reads a JSON snapshot from `path` and adds any nodes/edges not already
|
|
||||||
* present in the in-memory graph. Dedup is by node id (for nodes) and by
|
|
||||||
* (from_id, to_id, relation) tuple (for edges).
|
|
||||||
*
|
|
||||||
* Returns (as an EL int) the count of new nodes added. Embeddings are
|
|
||||||
* intentionally skipped on merged nodes to avoid Ollama delays at runtime;
|
|
||||||
* auto_link_semantic will handle them when nodes are next activated.
|
|
||||||
*
|
|
||||||
* Does not merge layers — the in-process layer registry is authoritative. */
|
|
||||||
el_val_t engram_load_merge(el_val_t path) {
|
|
||||||
const char* p = EL_CSTR(path);
|
|
||||||
if (!p || !*p) return 0;
|
|
||||||
FILE* f = fopen(p, "rb");
|
|
||||||
if (!f) return 0;
|
|
||||||
fseek(f, 0, SEEK_END);
|
|
||||||
long sz = ftell(f);
|
|
||||||
rewind(f);
|
|
||||||
if (sz <= 0) { fclose(f); return 0; }
|
|
||||||
char* data = malloc((size_t)sz + 1);
|
|
||||||
if (!data) { fclose(f); return 0; }
|
|
||||||
size_t got = fread(data, 1, (size_t)sz, f);
|
|
||||||
fclose(f);
|
|
||||||
data[got] = '\0';
|
|
||||||
|
|
||||||
EngramStore* g = engram_get();
|
|
||||||
int64_t added_nodes = 0;
|
|
||||||
|
|
||||||
/* Walk nodes array — skip any node whose id already exists */
|
|
||||||
const char* nodes_p = json_find_key(data, "nodes");
|
|
||||||
if (nodes_p) {
|
|
||||||
nodes_p = eg_skip_ws(nodes_p);
|
|
||||||
if (*nodes_p == '[') {
|
|
||||||
nodes_p++;
|
|
||||||
nodes_p = eg_skip_ws(nodes_p);
|
|
||||||
while (*nodes_p && *nodes_p != ']') {
|
|
||||||
if (*nodes_p != '{') { nodes_p++; continue; }
|
|
||||||
const char* end = json_skip_value(nodes_p);
|
|
||||||
size_t n = (size_t)(end - nodes_p);
|
|
||||||
char* obj = malloc(n + 1);
|
|
||||||
memcpy(obj, nodes_p, n); obj[n] = '\0';
|
|
||||||
char* nid = eg_get_str_field(obj, "id");
|
|
||||||
int already = (nid && *nid && engram_find_node(nid) != NULL);
|
|
||||||
free(nid);
|
|
||||||
if (!already) {
|
|
||||||
engram_grow_nodes();
|
|
||||||
EngramNode* nn = &g->nodes[g->node_count];
|
|
||||||
memset(nn, 0, sizeof(*nn));
|
|
||||||
nn->id = eg_get_str_field(obj, "id");
|
|
||||||
nn->content = eg_get_str_field(obj, "content");
|
|
||||||
nn->node_type = eg_get_str_field(obj, "node_type");
|
|
||||||
nn->label = eg_get_str_field(obj, "label");
|
|
||||||
nn->tier = eg_get_str_field(obj, "tier");
|
|
||||||
nn->tags = eg_get_str_field(obj, "tags");
|
|
||||||
nn->metadata = eg_get_str_field(obj, "metadata");
|
|
||||||
if (!nn->metadata || !*nn->metadata) { free(nn->metadata); nn->metadata = strdup("{}"); }
|
|
||||||
nn->salience = eg_get_num_field(obj, "salience");
|
|
||||||
nn->importance = eg_get_num_field(obj, "importance");
|
|
||||||
nn->confidence = eg_get_num_field(obj, "confidence");
|
|
||||||
nn->temporal_decay_rate = eg_get_num_field(obj, "temporal_decay_rate");
|
|
||||||
nn->activation_count = eg_get_int_field(obj, "activation_count");
|
|
||||||
nn->last_activated = eg_get_int_field(obj, "last_activated");
|
|
||||||
nn->created_at = eg_get_int_field(obj, "created_at");
|
|
||||||
nn->updated_at = eg_get_int_field(obj, "updated_at");
|
|
||||||
nn->background_activation = eg_get_num_field(obj, "background_activation");
|
|
||||||
nn->working_memory_weight = eg_get_num_field(obj, "working_memory_weight");
|
|
||||||
if (!isfinite(nn->working_memory_weight) || nn->working_memory_weight < 0.0 || nn->working_memory_weight > 1.0)
|
|
||||||
nn->working_memory_weight = 0.0; /* clamp corrupt snapshot values */
|
|
||||||
nn->suppression_count = (int32_t)eg_get_int_field(obj, "suppression_count");
|
|
||||||
if (json_find_key(obj, "layer_id")) {
|
|
||||||
nn->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id");
|
|
||||||
} else {
|
|
||||||
nn->layer_id = ENGRAM_LAYER_DEFAULT;
|
|
||||||
}
|
|
||||||
g->node_count++;
|
|
||||||
added_nodes++;
|
|
||||||
}
|
|
||||||
free(obj);
|
|
||||||
nodes_p = end;
|
|
||||||
nodes_p = eg_skip_ws(nodes_p);
|
|
||||||
if (*nodes_p == ',') { nodes_p++; nodes_p = eg_skip_ws(nodes_p); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Walk edges array — skip if (from_id, to_id, relation) already present */
|
|
||||||
const char* edges_p = json_find_key(data, "edges");
|
|
||||||
if (edges_p) {
|
|
||||||
edges_p = eg_skip_ws(edges_p);
|
|
||||||
if (*edges_p == '[') {
|
|
||||||
edges_p++;
|
|
||||||
edges_p = eg_skip_ws(edges_p);
|
|
||||||
while (*edges_p && *edges_p != ']') {
|
|
||||||
if (*edges_p != '{') { edges_p++; continue; }
|
|
||||||
const char* end = json_skip_value(edges_p);
|
|
||||||
size_t n = (size_t)(end - edges_p);
|
|
||||||
char* obj = malloc(n + 1);
|
|
||||||
memcpy(obj, edges_p, n); obj[n] = '\0';
|
|
||||||
char* efrom = eg_get_str_field(obj, "from_id");
|
|
||||||
char* eto = eg_get_str_field(obj, "to_id");
|
|
||||||
char* erel = eg_get_str_field(obj, "relation");
|
|
||||||
/* Check for duplicate by scanning existing edges */
|
|
||||||
int dup = 0;
|
|
||||||
if (efrom && eto && erel) {
|
|
||||||
for (int64_t ei = 0; ei < g->edge_count; ei++) {
|
|
||||||
EngramEdge* ex = &g->edges[ei];
|
|
||||||
if (ex->from_id && ex->to_id && ex->relation &&
|
|
||||||
strcmp(ex->from_id, efrom) == 0 &&
|
|
||||||
strcmp(ex->to_id, eto) == 0 &&
|
|
||||||
strcmp(ex->relation, erel) == 0) {
|
|
||||||
dup = 1; break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!dup) {
|
|
||||||
engram_grow_edges();
|
|
||||||
EngramEdge* ee = &g->edges[g->edge_count];
|
|
||||||
memset(ee, 0, sizeof(*ee));
|
|
||||||
ee->id = eg_get_str_field(obj, "id");
|
|
||||||
ee->from_id = efrom ? efrom : strdup("");
|
|
||||||
ee->to_id = eto ? eto : strdup("");
|
|
||||||
ee->relation = erel ? erel : strdup("");
|
|
||||||
ee->metadata = eg_get_str_field(obj, "metadata");
|
|
||||||
if (!ee->metadata || !*ee->metadata) { free(ee->metadata); ee->metadata = strdup("{}"); }
|
|
||||||
ee->weight = eg_get_num_field(obj, "weight");
|
|
||||||
ee->confidence = eg_get_num_field(obj, "confidence");
|
|
||||||
ee->created_at = eg_get_int_field(obj, "created_at");
|
|
||||||
ee->updated_at = eg_get_int_field(obj, "updated_at");
|
|
||||||
ee->last_fired = eg_get_int_field(obj, "last_fired");
|
|
||||||
ee->inhibitory = (int)eg_get_int_field(obj, "inhibitory");
|
|
||||||
if (json_find_key(obj, "layer_id")) {
|
|
||||||
ee->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id");
|
|
||||||
} else {
|
|
||||||
ee->layer_id = ENGRAM_LAYER_DEFAULT;
|
|
||||||
}
|
|
||||||
g->edge_count++;
|
|
||||||
/* NOTE: efrom/eto/erel ownership transferred to ee above */
|
|
||||||
efrom = NULL; eto = NULL; erel = NULL;
|
|
||||||
} else {
|
|
||||||
free(efrom); free(eto); free(erel);
|
|
||||||
}
|
|
||||||
free(obj);
|
|
||||||
edges_p = end;
|
|
||||||
edges_p = eg_skip_ws(edges_p);
|
|
||||||
if (*edges_p == ',') { edges_p++; edges_p = eg_skip_ws(edges_p); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
free(data);
|
|
||||||
return (el_val_t)added_nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
el_val_t engram_wm_count(void) {
|
|
||||||
EngramStore* g = engram_get();
|
|
||||||
int64_t count = 0;
|
|
||||||
for (int64_t i = 0; i < g->node_count; i++) {
|
|
||||||
if (g->nodes[i].working_memory_weight > 0.0) count++;
|
|
||||||
}
|
|
||||||
return (el_val_t)count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Average working_memory_weight across all promoted nodes (wm > 0).
|
|
||||||
* Returns the float bit-pattern via el_from_float so EL can use it with
|
|
||||||
* float_to_str / float_gt. Returns 0.0 when no nodes are promoted.
|
|
||||||
* Useful in heartbeat ISEs to distinguish "many weak activations" (sparse
|
|
||||||
* graph, low avg) from "few strong activations" (dense subgraph, high avg).
|
|
||||||
* Added 2026-06-04 self-review for graph health observability. */
|
|
||||||
el_val_t engram_wm_avg_weight(void) {
|
|
||||||
EngramStore* g = engram_get();
|
|
||||||
double sum = 0.0;
|
|
||||||
int64_t count = 0;
|
|
||||||
for (int64_t i = 0; i < g->node_count; i++) {
|
|
||||||
double w = g->nodes[i].working_memory_weight;
|
|
||||||
/* Defensive guard: skip any corrupt/out-of-range values so a single
|
|
||||||
* bad snapshot node doesn't produce a garbage average (e.g. 1.77e+234). */
|
|
||||||
if (w > 0.0 && w <= 1.0 && isfinite(w)) { sum += w; count++; }
|
|
||||||
}
|
|
||||||
double avg = (count > 0) ? (sum / (double)count) : 0.0;
|
|
||||||
return el_from_float(avg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* engram_wm_top_json — return top N working-memory nodes (by wm weight) as a
|
|
||||||
* compact JSON array for ISE heartbeat reporting.
|
|
||||||
*
|
|
||||||
* Each element: {"label":"...","node_type":"...","tier":"...","wm":0.42}
|
|
||||||
*
|
|
||||||
* Purpose: the heartbeat ISE reports wm_active (count) and wm_avg_weight but
|
|
||||||
* gives zero visibility into WM *composition* — which types/tiers are active.
|
|
||||||
* After long uptime every WM slot is in steady-state decay+re-promotion so
|
|
||||||
* wm_promotion ISEs never fire (they only fire on 0→>0.1 transitions).
|
|
||||||
* This function fills the observability gap by snapshotting the current top-N
|
|
||||||
* WM nodes on every heartbeat. Inserted 2026-06-05 self-review. */
|
|
||||||
el_val_t engram_wm_top_json(el_val_t n_v) {
|
|
||||||
int64_t top_n = (int64_t)n_v;
|
|
||||||
if (top_n <= 0) top_n = 10;
|
|
||||||
if (top_n > 50) top_n = 50;
|
|
||||||
EngramStore* g = engram_get();
|
|
||||||
|
|
||||||
/* Collect indices of promoted nodes, excluding monitoring noise.
|
|
||||||
* InternalStateEvent nodes are system-observation artifacts — they reflect
|
|
||||||
* what the daemon is doing, not what it knows. Including them in wm_top
|
|
||||||
* buries real knowledge (Memory, Knowledge, Belief nodes) under a wall of
|
|
||||||
* heartbeat/curiosity ISEs, making the heartbeat ISE useless for diagnosing
|
|
||||||
* WM composition. Filter them out here so wm_top always shows substantive
|
|
||||||
* content. (2026-06-07 self-review) */
|
|
||||||
int64_t* idx = malloc((size_t)(g->node_count + 1) * sizeof(int64_t));
|
|
||||||
if (!idx) return el_wrap_str(el_strdup("[]"));
|
|
||||||
int64_t mc = 0;
|
|
||||||
for (int64_t i = 0; i < g->node_count; i++) {
|
|
||||||
if (g->nodes[i].working_memory_weight > 0.0) {
|
|
||||||
const char* nt = g->nodes[i].node_type;
|
|
||||||
if (nt && strcmp(nt, "InternalStateEvent") == 0) continue;
|
|
||||||
idx[mc++] = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Insertion-sort descending by wm weight (mc is typically small). */
|
|
||||||
for (int64_t i = 1; i < mc; i++) {
|
|
||||||
int64_t key = idx[i];
|
|
||||||
double kw = g->nodes[key].working_memory_weight;
|
|
||||||
int64_t j = i;
|
|
||||||
while (j > 0 && g->nodes[idx[j-1]].working_memory_weight < kw) {
|
|
||||||
idx[j] = idx[j-1]; j--;
|
|
||||||
}
|
|
||||||
idx[j] = key;
|
|
||||||
}
|
|
||||||
|
|
||||||
int64_t emit = mc < top_n ? mc : top_n;
|
|
||||||
JsonBuf b; jb_init(&b);
|
|
||||||
jb_putc(&b, '[');
|
|
||||||
for (int64_t k = 0; k < emit; k++) {
|
|
||||||
EngramNode* n = &g->nodes[idx[k]];
|
|
||||||
if (k > 0) jb_putc(&b, ',');
|
|
||||||
jb_putc(&b, '{');
|
|
||||||
jb_puts(&b, "\"label\":");
|
|
||||||
jb_emit_escaped(&b, n->label ? n->label : "");
|
|
||||||
jb_puts(&b, ",\"node_type\":");
|
|
||||||
jb_emit_escaped(&b, n->node_type ? n->node_type : "");
|
|
||||||
jb_puts(&b, ",\"tier\":");
|
|
||||||
jb_emit_escaped(&b, n->tier ? n->tier : "");
|
|
||||||
char tmp[48];
|
|
||||||
snprintf(tmp, sizeof(tmp), ",\"wm\":%.3f", n->working_memory_weight);
|
|
||||||
jb_puts(&b, tmp);
|
|
||||||
jb_putc(&b, '}');
|
|
||||||
}
|
|
||||||
free(idx);
|
|
||||||
jb_putc(&b, ']');
|
|
||||||
return el_wrap_str(b.buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef HAVE_CURL
|
#ifdef HAVE_CURL
|
||||||
/* ── DHARMA network ─────────────────────────────────────────────────────────
|
/* ── DHARMA network ─────────────────────────────────────────────────────────
|
||||||
* Real implementation. Peers are addressed by `dharma_id` — either bare
|
* Real implementation. Peers are addressed by `dharma_id` — either bare
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ el_val_t http_post_json_with_headers(el_val_t url, el_val_t headers_map, el_val
|
|||||||
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
|
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
|
||||||
el_val_t http_delete(el_val_t url);
|
el_val_t http_delete(el_val_t url);
|
||||||
el_val_t http_serve(el_val_t port, el_val_t handler);
|
el_val_t http_serve(el_val_t port, el_val_t handler);
|
||||||
|
void http_serve_async(el_val_t port, el_val_t handler);
|
||||||
el_val_t http_set_handler(el_val_t name);
|
el_val_t http_set_handler(el_val_t name);
|
||||||
|
|
||||||
/* HTTP server v2 ─────────────────────────────────────────────────────────────
|
/* HTTP server v2 ─────────────────────────────────────────────────────────────
|
||||||
@@ -176,7 +177,6 @@ el_val_t http_set_handler(el_val_t name);
|
|||||||
* existing handlers (e.g. products/web/server.el): it dispatches with
|
* existing handlers (e.g. products/web/server.el): it dispatches with
|
||||||
* (method, path, body), hardcodes 200 OK, and auto-detects content type. */
|
* (method, path, body), hardcodes 200 OK, and auto-detects content type. */
|
||||||
el_val_t http_serve_v2(el_val_t port, el_val_t handler);
|
el_val_t http_serve_v2(el_val_t port, el_val_t handler);
|
||||||
void http_serve_async(el_val_t port, el_val_t handler);
|
|
||||||
el_val_t http_set_handler_v2(el_val_t name);
|
el_val_t http_set_handler_v2(el_val_t name);
|
||||||
|
|
||||||
/* Build an HTTP response envelope. `headers_json` should be a JSON object
|
/* Build an HTTP response envelope. `headers_json` should be a JSON object
|
||||||
@@ -633,17 +633,17 @@ el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t d
|
|||||||
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
|
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
|
||||||
el_val_t engram_stats_json(void);
|
el_val_t engram_stats_json(void);
|
||||||
el_val_t engram_list_layers_json(void);
|
el_val_t engram_list_layers_json(void);
|
||||||
/* engram_compile_layered_json — produce a prompt-ready text block split
|
|
||||||
* into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire)
|
|
||||||
* and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if
|
|
||||||
* no nodes promoted to working memory. */
|
|
||||||
el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth);
|
|
||||||
|
|
||||||
/* ── Working memory ──────────────────────────────────────────────────────────*/
|
/* ── Working memory ──────────────────────────────────────────────────────────*/
|
||||||
el_val_t engram_wm_count(void);
|
el_val_t engram_wm_count(void);
|
||||||
el_val_t engram_wm_avg_weight(void);
|
el_val_t engram_wm_avg_weight(void);
|
||||||
el_val_t engram_wm_top_json(el_val_t n);
|
el_val_t engram_wm_top_json(el_val_t n);
|
||||||
el_val_t engram_load_merge(el_val_t path);
|
el_val_t engram_load_merge(el_val_t path);
|
||||||
|
/* engram_compile_layered_json — produce a prompt-ready text block split
|
||||||
|
* into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire)
|
||||||
|
* and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if
|
||||||
|
* no nodes promoted to working memory. */
|
||||||
|
el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth);
|
||||||
|
|
||||||
/* ── LLM (Anthropic API client) ─────────────────────────────────────────────
|
/* ── LLM (Anthropic API client) ─────────────────────────────────────────────
|
||||||
* All functions call https://api.anthropic.com/v1/messages with the API key
|
* All functions call https://api.anthropic.com/v1/messages with the API key
|
||||||
|
|||||||
@@ -5470,35 +5470,9 @@ void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
|
|||||||
#define ENGRAM_WM_THRESHOLD 0.15
|
#define ENGRAM_WM_THRESHOLD 0.15
|
||||||
#define ENGRAM_WM_DECAY 0.7
|
#define ENGRAM_WM_DECAY 0.7
|
||||||
#define ENGRAM_SUPPRESSION_BREAKTHROUGH 5
|
#define ENGRAM_SUPPRESSION_BREAKTHROUGH 5
|
||||||
/* ENGRAM_BREAKTHROUGH_WEIGHT: lowered 0.25→0.10 (2026-06-30 self-review, porting
|
#define ENGRAM_BREAKTHROUGH_WEIGHT 0.25
|
||||||
* fix from self-review 2026-06-26 branch). With 0.25, Knowledge nodes (threshold
|
|
||||||
* 0.15) promoted at ~0.21 decay in one call to ~0.147, fall below the 0.25 floor,
|
|
||||||
* and immediately lose their WM slot to fresh breakthrough candidates at 0.25.
|
|
||||||
* Natural promotion was invisible: live data showed 524/525 WM nodes at 0.25
|
|
||||||
* breakthrough floor. With 0.10, all per-type thresholds (minimum 0.15 Canonical)
|
|
||||||
* exceed the floor, so naturally-promoted nodes survive multiple decay cycles.
|
|
||||||
* Invariant maintained: BREAKTHROUGH_WEIGHT < min(type_thresholds). */
|
|
||||||
#define ENGRAM_BREAKTHROUGH_WEIGHT 0.10
|
|
||||||
/* ENGRAM_WM_CAP: hard limit on concurrent working-memory nodes (2026-06-30
|
|
||||||
* self-review, porting fix from self-review 2026-06-26 branch). Without this,
|
|
||||||
* broad curiosity seeds like "knowledge" promote 500+ nodes simultaneously —
|
|
||||||
* wm_avg_weight collapses to the breakthrough floor, goal-bias differentiation
|
|
||||||
* is lost, and heartbeat ISEs show useless WM composition data. Cognitive
|
|
||||||
* basis: WM capacity is ~4 chunks (Cowan 2001); 24 allows richer multi-topic
|
|
||||||
* context while preventing flooding. Enforced in Pass 4 (per-call) and Pass 5
|
|
||||||
* (global across prior-promoted nodes). */
|
|
||||||
#define ENGRAM_WM_CAP 24
|
|
||||||
#define ENGRAM_INHIBITION_FACTOR 0.1
|
#define ENGRAM_INHIBITION_FACTOR 0.1
|
||||||
|
|
||||||
/* qsort comparator — descending double, used by WM cap enforcement. */
|
|
||||||
static int engram_cmp_double_desc(const void* a, const void* b) {
|
|
||||||
double da = *(const double*)a;
|
|
||||||
double db = *(const double*)b;
|
|
||||||
if (da > db) return -1;
|
|
||||||
if (da < db) return 1;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Layered consciousness architecture ──────────────────────────────────────
|
/* ── Layered consciousness architecture ──────────────────────────────────────
|
||||||
*
|
*
|
||||||
* The engram graph is stratified into LAYERS that gate which suppressions
|
* The engram graph is stratified into LAYERS that gate which suppressions
|
||||||
@@ -6685,19 +6659,6 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
|||||||
for (int64_t i = 0; i < g->node_count; i++) {
|
for (int64_t i = 0; i < g->node_count; i++) {
|
||||||
if (!reached[i] || best_bg[i] <= 0.0) continue;
|
if (!reached[i] || best_bg[i] <= 0.0) continue;
|
||||||
EngramNode* n = &g->nodes[i];
|
EngramNode* n = &g->nodes[i];
|
||||||
/* InternalStateEvent nodes are observability-only — never admit to WM.
|
|
||||||
* Their JSON content (curiosity seeds, heartbeat payloads) contains common
|
|
||||||
* words that trigger lexical seeding (e.g. "knowledge" in curiosity ISEs),
|
|
||||||
* leading to repeated suppression and eventual breakthrough at the floor.
|
|
||||||
* ISEs surfacing in context compilation are noise, not signal. Clear their
|
|
||||||
* suppression_count so they don't build toward breakthrough, then skip.
|
|
||||||
* (2026-06-30 self-review: porting fix from 2026-06-26 branch; SYNAPSE
|
|
||||||
* paper confirms WM should hold only semantically relevant content.) */
|
|
||||||
if (n->node_type && strcmp(n->node_type, "InternalStateEvent") == 0) {
|
|
||||||
n->suppression_count = 0;
|
|
||||||
wm_weights[i] = 0.0;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
/* Per-type threshold: safety nodes break through more easily. */
|
/* Per-type threshold: safety nodes break through more easily. */
|
||||||
double type_threshold = engram_type_threshold(n->node_type, n->tier);
|
double type_threshold = engram_type_threshold(n->node_type, n->tier);
|
||||||
/* Goal bias weights the node's relevance to current intent. */
|
/* Goal bias weights the node's relevance to current intent. */
|
||||||
@@ -6749,124 +6710,10 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
|||||||
n->suppression_count = 0;
|
n->suppression_count = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── PASS 4: WM capacity cap (per-call) ─────────────────────────────────
|
/* Persist working_memory_weight (post Pass 3) to node store. */
|
||||||
* Enforce ENGRAM_WM_CAP as a hard upper bound on nodes promoted in this
|
|
||||||
* activation call. Without this, broad curiosity seeds like "knowledge"
|
|
||||||
* promote 500+ nodes simultaneously — wm_avg_weight collapses to the
|
|
||||||
* breakthrough floor, goal-bias differentiation is lost, and working memory
|
|
||||||
* becomes useless. (Ported from 2026-06-26 self-review branch; observed
|
|
||||||
* 525 promoted for "knowledge", 524 at breakthrough floor 0.25, 1 natural.) */
|
|
||||||
{
|
|
||||||
int64_t cap_count = 0;
|
|
||||||
for (int64_t i = 0; i < g->node_count; i++) {
|
for (int64_t i = 0; i < g->node_count; i++) {
|
||||||
if (wm_weights[i] > 0.0) cap_count++;
|
|
||||||
}
|
|
||||||
if (cap_count > ENGRAM_WM_CAP) {
|
|
||||||
double* cap_vals = malloc((size_t)cap_count * sizeof(double));
|
|
||||||
if (cap_vals) {
|
|
||||||
int64_t ci = 0;
|
|
||||||
for (int64_t i = 0; i < g->node_count; i++) {
|
|
||||||
if (wm_weights[i] > 0.0) cap_vals[ci++] = wm_weights[i];
|
|
||||||
}
|
|
||||||
qsort(cap_vals, (size_t)cap_count, sizeof(double),
|
|
||||||
engram_cmp_double_desc);
|
|
||||||
/* cap_vals[ENGRAM_WM_CAP-1] is the lowest weight that still
|
|
||||||
* fits inside the cap when sorted descending. */
|
|
||||||
double cutoff = cap_vals[ENGRAM_WM_CAP - 1];
|
|
||||||
free(cap_vals);
|
|
||||||
/* Count strictly above cutoff to handle ties correctly. */
|
|
||||||
int64_t above = 0;
|
|
||||||
for (int64_t i = 0; i < g->node_count; i++) {
|
|
||||||
if (wm_weights[i] > cutoff) above++;
|
|
||||||
}
|
|
||||||
int64_t at_cutoff_slots = ENGRAM_WM_CAP - above;
|
|
||||||
/* Evict nodes that don't make the cut. */
|
|
||||||
for (int64_t i = 0; i < g->node_count; i++) {
|
|
||||||
if (wm_weights[i] <= 0.0) continue; /* not promoted */
|
|
||||||
if (wm_weights[i] > cutoff) continue; /* above cutoff */
|
|
||||||
if (at_cutoff_slots > 0) {
|
|
||||||
at_cutoff_slots--;
|
|
||||||
continue; /* fills a slot */
|
|
||||||
}
|
|
||||||
wm_weights[i] = 0.0; /* over cap: evict */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* If malloc failed, skip cap — WM unbounded this call, no corruption. */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Persist working_memory_weight (post Pass 4) to node store.
|
|
||||||
*
|
|
||||||
* Conversational thread continuity (ENGRAM_WM_DECAY):
|
|
||||||
* Nodes promoted in a previous turn but NOT reached by the current BFS
|
|
||||||
* fan-out retain a decayed weight rather than being zeroed. This models
|
|
||||||
* the brain's ability to maintain recent context across successive turns
|
|
||||||
* without requiring explicit re-activation. A node that was relevant one
|
|
||||||
* query ago stays weakly present in working memory; a node from two
|
|
||||||
* queries ago retains 0.7² ≈ 0.49 of its original weight; after ~5 quiet
|
|
||||||
* turns it falls below 0.01 and is effectively evicted (set to 0.0).
|
|
||||||
*
|
|
||||||
* NOTE: this was documented in the ENGRAM_WM_DECAY constant comment since
|
|
||||||
* the two-layer architecture was introduced, but was never implemented —
|
|
||||||
* unreached nodes were always zeroed unconditionally. Fixed 2026-06-30
|
|
||||||
* self-review. */
|
|
||||||
for (int64_t i = 0; i < g->node_count; i++) {
|
|
||||||
if (!reached[i] && g->nodes[i].working_memory_weight > 0.0) {
|
|
||||||
/* Carry-over decay: node held WM weight from prior activation but
|
|
||||||
* the current query's BFS fan-out did not reach it. Apply decay
|
|
||||||
* rather than zero so recently-active context persists. */
|
|
||||||
double decayed = g->nodes[i].working_memory_weight * ENGRAM_WM_DECAY;
|
|
||||||
g->nodes[i].working_memory_weight = (decayed < 0.01) ? 0.0 : decayed;
|
|
||||||
} else {
|
|
||||||
g->nodes[i].working_memory_weight = wm_weights[i];
|
g->nodes[i].working_memory_weight = wm_weights[i];
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/* ── PASS 5: Global WM cap enforcement ───────────────────────────────────
|
|
||||||
* Pass 4 capped this call's new candidates. But nodes already in WM from
|
|
||||||
* prior calls retain their persisted working_memory_weight (via the decay
|
|
||||||
* carry-over above). Over multiple activation calls total WM can grow well
|
|
||||||
* above ENGRAM_WM_CAP. This pass enforces the cap globally across ALL
|
|
||||||
* nodes in the store, keeping only the top ENGRAM_WM_CAP by current weight.
|
|
||||||
* Correct cognitive model: WM capacity is global (Cowan 2001); more recent
|
|
||||||
* activations outcompete older decayed ones. (Ported from 2026-06-26
|
|
||||||
* self-review branch.) */
|
|
||||||
{
|
|
||||||
int64_t global_wm_count = 0;
|
|
||||||
for (int64_t i = 0; i < g->node_count; i++) {
|
|
||||||
if (g->nodes[i].working_memory_weight > 0.0) global_wm_count++;
|
|
||||||
}
|
|
||||||
if (global_wm_count > ENGRAM_WM_CAP) {
|
|
||||||
double* gvals = malloc((size_t)global_wm_count * sizeof(double));
|
|
||||||
if (gvals) {
|
|
||||||
int64_t gi = 0;
|
|
||||||
for (int64_t i = 0; i < g->node_count; i++) {
|
|
||||||
if (g->nodes[i].working_memory_weight > 0.0)
|
|
||||||
gvals[gi++] = g->nodes[i].working_memory_weight;
|
|
||||||
}
|
|
||||||
qsort(gvals, (size_t)global_wm_count, sizeof(double),
|
|
||||||
engram_cmp_double_desc);
|
|
||||||
double gcutoff = gvals[ENGRAM_WM_CAP - 1];
|
|
||||||
free(gvals);
|
|
||||||
int64_t gabove = 0;
|
|
||||||
for (int64_t i = 0; i < g->node_count; i++) {
|
|
||||||
if (g->nodes[i].working_memory_weight > gcutoff) gabove++;
|
|
||||||
}
|
|
||||||
int64_t gslots_at_cutoff = ENGRAM_WM_CAP - gabove;
|
|
||||||
for (int64_t i = 0; i < g->node_count; i++) {
|
|
||||||
EngramNode* n = &g->nodes[i];
|
|
||||||
if (n->working_memory_weight <= 0.0) continue;
|
|
||||||
if (n->working_memory_weight > gcutoff) continue;
|
|
||||||
if (gslots_at_cutoff > 0) {
|
|
||||||
gslots_at_cutoff--;
|
|
||||||
continue; /* fills a slot */
|
|
||||||
}
|
|
||||||
n->working_memory_weight = 0.0; /* evict: over global cap */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* If malloc failed, skip — WM over cap this call, no data corruption. */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Collect all background-activated nodes for the return value ────
|
/* ── Collect all background-activated nodes for the return value ────
|
||||||
* Callers see both layers. Context compilation uses only promoted nodes
|
* Callers see both layers. Context compilation uses only promoted nodes
|
||||||
@@ -7234,156 +7081,6 @@ el_val_t engram_load(el_val_t path) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* engram_load_merge — like engram_load but WITHOUT resetting the store.
|
|
||||||
* Reads a JSON snapshot from `path` and adds any nodes/edges not already
|
|
||||||
* present in the in-memory graph. Dedup is by node id (for nodes) and by
|
|
||||||
* (from_id, to_id, relation) tuple (for edges).
|
|
||||||
*
|
|
||||||
* Returns (as an EL int) the count of new nodes added. Used by the soul
|
|
||||||
* daemon's periodic refresh cycle to keep its in-process Engram in sync
|
|
||||||
* with the HTTP Engram store without losing current working memory state.
|
|
||||||
* Ported from el-compiler/runtime on 2026-06-30 self-review. */
|
|
||||||
el_val_t engram_load_merge(el_val_t path) {
|
|
||||||
const char* p = EL_CSTR(path);
|
|
||||||
if (!p || !*p) return 0;
|
|
||||||
FILE* f = fopen(p, "rb");
|
|
||||||
if (!f) return 0;
|
|
||||||
fseek(f, 0, SEEK_END);
|
|
||||||
long sz = ftell(f);
|
|
||||||
rewind(f);
|
|
||||||
if (sz <= 0) { fclose(f); return 0; }
|
|
||||||
char* data = malloc((size_t)sz + 1);
|
|
||||||
if (!data) { fclose(f); return 0; }
|
|
||||||
size_t got = fread(data, 1, (size_t)sz, f);
|
|
||||||
fclose(f);
|
|
||||||
data[got] = '\0';
|
|
||||||
|
|
||||||
EngramStore* g = engram_get();
|
|
||||||
int64_t added_nodes = 0;
|
|
||||||
|
|
||||||
/* Walk nodes array — skip any node whose id already exists */
|
|
||||||
const char* nodes_p = json_find_key(data, "nodes");
|
|
||||||
if (nodes_p) {
|
|
||||||
nodes_p = eg_skip_ws(nodes_p);
|
|
||||||
if (*nodes_p == '[') {
|
|
||||||
nodes_p++;
|
|
||||||
nodes_p = eg_skip_ws(nodes_p);
|
|
||||||
while (*nodes_p && *nodes_p != ']') {
|
|
||||||
if (*nodes_p != '{') { nodes_p++; continue; }
|
|
||||||
const char* end = json_skip_value(nodes_p);
|
|
||||||
size_t n = (size_t)(end - nodes_p);
|
|
||||||
char* obj = malloc(n + 1);
|
|
||||||
memcpy(obj, nodes_p, n); obj[n] = '\0';
|
|
||||||
char* nid = eg_get_str_field(obj, "id");
|
|
||||||
int already = (nid && *nid && engram_find_node(nid) != NULL);
|
|
||||||
free(nid);
|
|
||||||
if (!already) {
|
|
||||||
engram_grow_nodes();
|
|
||||||
EngramNode* nn = &g->nodes[g->node_count];
|
|
||||||
memset(nn, 0, sizeof(*nn));
|
|
||||||
nn->id = eg_get_str_field(obj, "id");
|
|
||||||
nn->content = eg_get_str_field(obj, "content");
|
|
||||||
nn->node_type = eg_get_str_field(obj, "node_type");
|
|
||||||
nn->label = eg_get_str_field(obj, "label");
|
|
||||||
nn->tier = eg_get_str_field(obj, "tier");
|
|
||||||
nn->tags = eg_get_str_field(obj, "tags");
|
|
||||||
nn->metadata = eg_get_str_field(obj, "metadata");
|
|
||||||
if (!nn->metadata || !*nn->metadata) { free(nn->metadata); nn->metadata = strdup("{}"); }
|
|
||||||
nn->salience = eg_get_num_field(obj, "salience");
|
|
||||||
nn->importance = eg_get_num_field(obj, "importance");
|
|
||||||
nn->confidence = eg_get_num_field(obj, "confidence");
|
|
||||||
nn->temporal_decay_rate = eg_get_num_field(obj, "temporal_decay_rate");
|
|
||||||
nn->activation_count = eg_get_int_field(obj, "activation_count");
|
|
||||||
nn->last_activated = eg_get_int_field(obj, "last_activated");
|
|
||||||
nn->created_at = eg_get_int_field(obj, "created_at");
|
|
||||||
nn->updated_at = eg_get_int_field(obj, "updated_at");
|
|
||||||
nn->background_activation = eg_get_num_field(obj, "background_activation");
|
|
||||||
nn->working_memory_weight = eg_get_num_field(obj, "working_memory_weight");
|
|
||||||
if (!isfinite(nn->working_memory_weight) || nn->working_memory_weight < 0.0 || nn->working_memory_weight > 1.0)
|
|
||||||
nn->working_memory_weight = 0.0;
|
|
||||||
nn->suppression_count = (int32_t)eg_get_int_field(obj, "suppression_count");
|
|
||||||
if (json_find_key(obj, "layer_id")) {
|
|
||||||
nn->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id");
|
|
||||||
} else {
|
|
||||||
nn->layer_id = ENGRAM_LAYER_DEFAULT;
|
|
||||||
}
|
|
||||||
g->node_count++;
|
|
||||||
added_nodes++;
|
|
||||||
}
|
|
||||||
free(obj);
|
|
||||||
nodes_p = end;
|
|
||||||
nodes_p = eg_skip_ws(nodes_p);
|
|
||||||
if (*nodes_p == ',') { nodes_p++; nodes_p = eg_skip_ws(nodes_p); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Walk edges array — skip if (from_id, to_id, relation) already present */
|
|
||||||
const char* edges_p = json_find_key(data, "edges");
|
|
||||||
if (edges_p) {
|
|
||||||
edges_p = eg_skip_ws(edges_p);
|
|
||||||
if (*edges_p == '[') {
|
|
||||||
edges_p++;
|
|
||||||
edges_p = eg_skip_ws(edges_p);
|
|
||||||
while (*edges_p && *edges_p != ']') {
|
|
||||||
if (*edges_p != '{') { edges_p++; continue; }
|
|
||||||
const char* end = json_skip_value(edges_p);
|
|
||||||
size_t n = (size_t)(end - edges_p);
|
|
||||||
char* obj = malloc(n + 1);
|
|
||||||
memcpy(obj, edges_p, n); obj[n] = '\0';
|
|
||||||
char* efrom = eg_get_str_field(obj, "from_id");
|
|
||||||
char* eto = eg_get_str_field(obj, "to_id");
|
|
||||||
char* erel = eg_get_str_field(obj, "relation");
|
|
||||||
int dup = 0;
|
|
||||||
if (efrom && eto && erel) {
|
|
||||||
for (int64_t ei = 0; ei < g->edge_count; ei++) {
|
|
||||||
EngramEdge* ex = &g->edges[ei];
|
|
||||||
if (ex->from_id && ex->to_id && ex->relation &&
|
|
||||||
strcmp(ex->from_id, efrom) == 0 &&
|
|
||||||
strcmp(ex->to_id, eto) == 0 &&
|
|
||||||
strcmp(ex->relation, erel) == 0) {
|
|
||||||
dup = 1; break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!dup) {
|
|
||||||
engram_grow_edges();
|
|
||||||
EngramEdge* ee = &g->edges[g->edge_count];
|
|
||||||
memset(ee, 0, sizeof(*ee));
|
|
||||||
ee->id = eg_get_str_field(obj, "id");
|
|
||||||
ee->from_id = efrom ? efrom : strdup("");
|
|
||||||
ee->to_id = eto ? eto : strdup("");
|
|
||||||
ee->relation = erel ? erel : strdup("");
|
|
||||||
ee->metadata = eg_get_str_field(obj, "metadata");
|
|
||||||
if (!ee->metadata || !*ee->metadata) { free(ee->metadata); ee->metadata = strdup("{}"); }
|
|
||||||
ee->weight = eg_get_num_field(obj, "weight");
|
|
||||||
ee->confidence = eg_get_num_field(obj, "confidence");
|
|
||||||
ee->created_at = eg_get_int_field(obj, "created_at");
|
|
||||||
ee->updated_at = eg_get_int_field(obj, "updated_at");
|
|
||||||
ee->last_fired = eg_get_int_field(obj, "last_fired");
|
|
||||||
ee->inhibitory = (int)eg_get_int_field(obj, "inhibitory");
|
|
||||||
if (json_find_key(obj, "layer_id")) {
|
|
||||||
ee->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id");
|
|
||||||
} else {
|
|
||||||
ee->layer_id = ENGRAM_LAYER_DEFAULT;
|
|
||||||
}
|
|
||||||
g->edge_count++;
|
|
||||||
efrom = NULL; eto = NULL; erel = NULL;
|
|
||||||
} else {
|
|
||||||
free(efrom); free(eto); free(erel);
|
|
||||||
}
|
|
||||||
free(obj);
|
|
||||||
edges_p = end;
|
|
||||||
edges_p = eg_skip_ws(edges_p);
|
|
||||||
if (*edges_p == ',') { edges_p++; edges_p = eg_skip_ws(edges_p); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
free(data);
|
|
||||||
return (el_val_t)added_nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Engram JSON-string accessors ─────────────────────────────────────────
|
/* ── Engram JSON-string accessors ─────────────────────────────────────────
|
||||||
* These return pre-serialized JSON strings so callers (especially HTTP
|
* These return pre-serialized JSON strings so callers (especially HTTP
|
||||||
* handlers) don't have to round-trip ElList/ElMap through json_stringify
|
* handlers) don't have to round-trip ElList/ElMap through json_stringify
|
||||||
@@ -7601,95 +7298,6 @@ el_val_t engram_activate_json(el_val_t query, el_val_t depth) {
|
|||||||
return el_wrap_str(b.buf);
|
return el_wrap_str(b.buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Working memory introspection helpers ────────────────────────────────────
|
|
||||||
*
|
|
||||||
* These three functions give the soul daemon visibility into WM composition
|
|
||||||
* without re-running activation. Used in heartbeat ISEs and curiosity scans.
|
|
||||||
* Ported from el-compiler/runtime to releases/v1.0.0-20260501 on 2026-06-30
|
|
||||||
* self-review (they were missing from the release build, breaking soul daemon
|
|
||||||
* compilation). */
|
|
||||||
|
|
||||||
el_val_t engram_wm_count(void) {
|
|
||||||
EngramStore* g = engram_get();
|
|
||||||
int64_t count = 0;
|
|
||||||
for (int64_t i = 0; i < g->node_count; i++) {
|
|
||||||
if (g->nodes[i].working_memory_weight > 0.0) count++;
|
|
||||||
}
|
|
||||||
return (el_val_t)count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Average working_memory_weight across all promoted nodes (wm > 0).
|
|
||||||
* Returns the float bit-pattern via el_from_float so EL can use it with
|
|
||||||
* float_to_str / float_gt. Returns 0.0 when no nodes are promoted.
|
|
||||||
* Useful in heartbeat ISEs to distinguish "many weak activations" from
|
|
||||||
* "few strong activations". Added 2026-06-04 self-review. */
|
|
||||||
el_val_t engram_wm_avg_weight(void) {
|
|
||||||
EngramStore* g = engram_get();
|
|
||||||
double sum = 0.0;
|
|
||||||
int64_t count = 0;
|
|
||||||
for (int64_t i = 0; i < g->node_count; i++) {
|
|
||||||
double w = g->nodes[i].working_memory_weight;
|
|
||||||
/* Skip corrupt/out-of-range values so a single bad snapshot node
|
|
||||||
* doesn't produce a garbage average. */
|
|
||||||
if (w > 0.0 && w <= 1.0 && isfinite(w)) { sum += w; count++; }
|
|
||||||
}
|
|
||||||
double avg = (count > 0) ? (sum / (double)count) : 0.0;
|
|
||||||
return el_from_float(avg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* engram_wm_top_json — return top N working-memory nodes (by wm weight) as a
|
|
||||||
* compact JSON array for ISE heartbeat reporting.
|
|
||||||
* Each element: {"label":"...","node_type":"...","tier":"...","wm":0.42}
|
|
||||||
* InternalStateEvent nodes are excluded — they're observation artifacts that
|
|
||||||
* would bury substantive WM content. Added 2026-06-05 self-review. */
|
|
||||||
el_val_t engram_wm_top_json(el_val_t n_v) {
|
|
||||||
int64_t top_n = (int64_t)n_v;
|
|
||||||
if (top_n <= 0) top_n = 10;
|
|
||||||
if (top_n > 50) top_n = 50;
|
|
||||||
EngramStore* g = engram_get();
|
|
||||||
int64_t* idx = malloc((size_t)(g->node_count + 1) * sizeof(int64_t));
|
|
||||||
if (!idx) return el_wrap_str(el_strdup("[]"));
|
|
||||||
int64_t mc = 0;
|
|
||||||
for (int64_t i = 0; i < g->node_count; i++) {
|
|
||||||
if (g->nodes[i].working_memory_weight > 0.0) {
|
|
||||||
const char* nt = g->nodes[i].node_type;
|
|
||||||
if (nt && strcmp(nt, "InternalStateEvent") == 0) continue;
|
|
||||||
idx[mc++] = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* Insertion-sort descending by wm weight (mc is typically small). */
|
|
||||||
for (int64_t i = 1; i < mc; i++) {
|
|
||||||
int64_t key = idx[i];
|
|
||||||
double kw = g->nodes[key].working_memory_weight;
|
|
||||||
int64_t j = i;
|
|
||||||
while (j > 0 && g->nodes[idx[j-1]].working_memory_weight < kw) {
|
|
||||||
idx[j] = idx[j-1]; j--;
|
|
||||||
}
|
|
||||||
idx[j] = key;
|
|
||||||
}
|
|
||||||
int64_t emit = mc < top_n ? mc : top_n;
|
|
||||||
JsonBuf b; jb_init(&b);
|
|
||||||
jb_putc(&b, '[');
|
|
||||||
for (int64_t k = 0; k < emit; k++) {
|
|
||||||
EngramNode* n = &g->nodes[idx[k]];
|
|
||||||
if (k > 0) jb_putc(&b, ',');
|
|
||||||
jb_putc(&b, '{');
|
|
||||||
jb_puts(&b, "\"label\":");
|
|
||||||
jb_emit_escaped(&b, n->label ? n->label : "");
|
|
||||||
jb_puts(&b, ",\"node_type\":");
|
|
||||||
jb_emit_escaped(&b, n->node_type ? n->node_type : "");
|
|
||||||
jb_puts(&b, ",\"tier\":");
|
|
||||||
jb_emit_escaped(&b, n->tier ? n->tier : "");
|
|
||||||
char tmp[48];
|
|
||||||
snprintf(tmp, sizeof(tmp), ",\"wm\":%.3f", n->working_memory_weight);
|
|
||||||
jb_puts(&b, tmp);
|
|
||||||
jb_putc(&b, '}');
|
|
||||||
}
|
|
||||||
free(idx);
|
|
||||||
jb_putc(&b, ']');
|
|
||||||
return el_wrap_str(b.buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
el_val_t engram_stats_json(void) {
|
el_val_t engram_stats_json(void) {
|
||||||
EngramStore* g = engram_get();
|
EngramStore* g = engram_get();
|
||||||
char buf[128];
|
char buf[128];
|
||||||
|
|||||||
@@ -601,13 +601,6 @@ el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t d
|
|||||||
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
|
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
|
||||||
el_val_t engram_stats_json(void);
|
el_val_t engram_stats_json(void);
|
||||||
el_val_t engram_list_layers_json(void);
|
el_val_t engram_list_layers_json(void);
|
||||||
/* Working memory introspection — count, mean weight, and top-N snapshot.
|
|
||||||
* Ported from el-compiler/runtime on 2026-06-30 self-review. */
|
|
||||||
el_val_t engram_wm_count(void);
|
|
||||||
el_val_t engram_wm_avg_weight(void);
|
|
||||||
el_val_t engram_wm_top_json(el_val_t n);
|
|
||||||
/* Merge-load: add nodes/edges from a snapshot without resetting the store. */
|
|
||||||
el_val_t engram_load_merge(el_val_t path);
|
|
||||||
/* engram_compile_layered_json — produce a prompt-ready text block split
|
/* engram_compile_layered_json — produce a prompt-ready text block split
|
||||||
* into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire)
|
* into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire)
|
||||||
* and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if
|
* and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if
|
||||||
|
|||||||
Reference in New Issue
Block a user