feat(runtime): port engram_load_merge to released runtime + add missing WM headers
engram_load_merge was added to el-compiler/runtime in35c1897but never ported to the released runtime used by Engram and the soul daemon. awareness.el calls engram_load_merge in its sync refresh cycle; without this function in lang/releases/v1.0.0-20260501/el_runtime.c the soul daemon fails to compile. Also adds header declarations for engram_wm_count, engram_wm_avg_weight, engram_wm_top_json, and engram_load_merge — all four were added as implementations (da116b2/35c1897) but their prototypes were missing from el_runtime.h, causing implicit-function-declaration warnings and potential ABI breakage on stricter compilers. Identified during self-review 2026-06-30.
This commit is contained in:
@@ -7234,6 +7234,156 @@ 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
|
||||||
|
|||||||
@@ -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_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