diff --git a/lang/releases/v1.0.0-20260501/el_runtime.c b/lang/releases/v1.0.0-20260501/el_runtime.c index 1aa4233..f7f78f6 100644 --- a/lang/releases/v1.0.0-20260501/el_runtime.c +++ b/lang/releases/v1.0.0-20260501/el_runtime.c @@ -7234,6 +7234,156 @@ el_val_t engram_load(el_val_t path) { 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 ───────────────────────────────────────── * These return pre-serialized JSON strings so callers (especially HTTP * handlers) don't have to round-trip ElList/ElMap through json_stringify diff --git a/lang/releases/v1.0.0-20260501/el_runtime.h b/lang/releases/v1.0.0-20260501/el_runtime.h index 72bbf4b..a3dd683 100644 --- a/lang/releases/v1.0.0-20260501/el_runtime.h +++ b/lang/releases/v1.0.0-20260501/el_runtime.h @@ -601,6 +601,13 @@ 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_stats_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 * into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire) * and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if