diff --git a/lang/releases/v1.0.0-20260501/el_runtime.c b/lang/releases/v1.0.0-20260501/el_runtime.c index 831e016..a4a2742 100644 --- a/lang/releases/v1.0.0-20260501/el_runtime.c +++ b/lang/releases/v1.0.0-20260501/el_runtime.c @@ -140,6 +140,12 @@ void println(el_val_t s) { const char* str = EL_CSTR(s); if (str) puts(str); else puts(""); + /* Flush stdout immediately so logs are visible even when stdout is redirected + * to a file (launchd plists redirect stdout; stdio defaults to full-buffering + * for non-tty streams, which silently holds log messages indefinitely). + * (2026-06-29 self-review: soul.out.log showed May 26 mtime despite daemon + * running since June 29 07:14AM — startup println output stuck in stdio buffer) */ + fflush(stdout); } void print(el_val_t s) { @@ -5562,6 +5568,13 @@ static double engram_type_threshold(const char* node_type, const char* tier) { if (strcmp(tier, "Lesson") == 0) return 0.25; } if (node_type) { + /* Knowledge nodes with Procedural/Semantic tier (post-activation-count migration) + * should keep a lower threshold than the generic 0.40 default. + * Canonical/Lesson Knowledge: handled by tier checks above (0.15/0.25). + * Procedural Knowledge (activation_count >= 50): 0.20 — proven repeated use + * justifies lower threshold, consistent with ACT-R base-level learning rationale. + * (2026-06-29 self-review: documented in knowledge graph since 2026-06-04) */ + if (strcmp(node_type, "Knowledge") == 0) return 0.20; if (strcmp(node_type, "Belief") == 0) return 0.30; if (strcmp(node_type, "Entity") == 0) return 0.30; } @@ -5830,11 +5843,15 @@ static void engram_grow_edges(void) { g->edge_capacity = nc; } -/* Build a fresh UUID string. Reuses uuid_new but takes the underlying char*. */ +/* Build a fresh UUID string. Reuses uuid_new but takes the underlying char*. + * Must use el_strdup_persist because the returned pointer is stored in EngramNode/EngramEdge + * fields that outlive the current HTTP request's per-request string arena. If el_strdup + * were used the arena teardown at el_request_end() would free the id/content/etc. strings, + * leaving dangling pointers in the EngramStore. (2026-06-27 fix: arena corruption bug) */ static char* engram_new_id(void) { el_val_t v = uuid_new(); const char* s = EL_CSTR(v); - return el_strdup(s ? s : ""); + return el_strdup_persist(s ? s : ""); } /* Convert a node into an ElMap of its fields. */ @@ -5877,11 +5894,14 @@ static double engram_decode_score(el_val_t v) { return (double)n; } +/* engram_first_n_chars — extract label substring from content. + * Must use persistent allocation since the result is stored in EngramNode.label. + * (2026-06-27 fix: arena corruption bug — was using el_strbuf/el_strdup) */ static char* engram_first_n_chars(const char* s, size_t n) { - if (!s) return el_strdup(""); + if (!s) return el_strdup_persist(""); size_t l = strlen(s); if (l > n) l = n; - char* out = el_strbuf(l); + char* out = el_strbuf_persist(l); memcpy(out, s, l); out[l] = '\0'; return out; @@ -5895,12 +5915,13 @@ el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience) { n->id = engram_new_id(); const char* c = EL_CSTR(content); const char* nt = EL_CSTR(node_type); - n->content = el_strdup(c ? c : ""); - n->node_type = el_strdup(nt && *nt ? nt : "Memory"); + /* Use el_strdup_persist: these strings outlive the HTTP request arena. */ + n->content = el_strdup_persist(c ? c : ""); + n->node_type = el_strdup_persist(nt && *nt ? nt : "Memory"); n->label = engram_first_n_chars(c, 60); - n->tier = el_strdup("Working"); - n->tags = el_strdup(""); - n->metadata = el_strdup("{}"); + n->tier = el_strdup_persist("Working"); + n->tags = el_strdup_persist(""); + n->metadata = el_strdup_persist("{}"); n->salience = engram_decode_score(salience); if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5; n->importance = 0.5; @@ -5913,7 +5934,7 @@ el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience) { n->updated_at = now; n->layer_id = ENGRAM_LAYER_DEFAULT; g->node_count++; - return el_wrap_str(el_strdup(n->id)); + return el_wrap_str(el_strdup_persist(n->id)); } el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, @@ -5929,12 +5950,13 @@ el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, const char* lb = EL_CSTR(label); const char* ti = EL_CSTR(tier); const char* tg = EL_CSTR(tags); - n->content = el_strdup(c ? c : ""); - n->node_type = el_strdup(nt && *nt ? nt : "Memory"); - n->label = el_strdup(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : "")); - n->tier = el_strdup(ti && *ti ? ti : "Working"); - n->tags = el_strdup(tg ? tg : ""); - n->metadata = el_strdup("{}"); + /* Use el_strdup_persist: these strings outlive the HTTP request arena. */ + n->content = el_strdup_persist(c ? c : ""); + n->node_type = el_strdup_persist(nt && *nt ? nt : "Memory"); + n->label = el_strdup_persist(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : "")); + n->tier = el_strdup_persist(ti && *ti ? ti : "Working"); + n->tags = el_strdup_persist(tg ? tg : ""); + n->metadata = el_strdup_persist("{}"); n->salience = engram_decode_score(salience); n->importance = engram_decode_score(importance); n->confidence = engram_decode_score(confidence); @@ -5949,7 +5971,7 @@ el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, n->updated_at = now; n->layer_id = ENGRAM_LAYER_DEFAULT; g->node_count++; - return el_wrap_str(el_strdup(n->id)); + return el_wrap_str(el_strdup_persist(n->id)); } /* engram_node_layered — like engram_node_full but with explicit layer @@ -5982,20 +6004,21 @@ el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t labe const char* lb = EL_CSTR(label); const char* tg = EL_CSTR(tags); const char* st = EL_CSTR(status); - n->content = el_strdup(c ? c : ""); - n->node_type = el_strdup(nt && *nt ? nt : "Memory"); - n->label = el_strdup(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : "")); - n->tier = el_strdup("Working"); - n->tags = el_strdup(tg ? tg : ""); + /* Use el_strdup_persist: these strings outlive the HTTP request arena. */ + n->content = el_strdup_persist(c ? c : ""); + n->node_type = el_strdup_persist(nt && *nt ? nt : "Memory"); + n->label = el_strdup_persist(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : "")); + n->tier = el_strdup_persist("Working"); + n->tags = el_strdup_persist(tg ? tg : ""); if (st && *st) { /* Minimal metadata payload: {"status":"..."}. Keep it cheap so * callers using `status` don't pay JSON parse cost on every read. */ size_t sl = strlen(st) + 16; - char* meta = el_strbuf(sl); + char* meta = el_strbuf_persist(sl); snprintf(meta, sl, "{\"status\":\"%s\"}", st); n->metadata = meta; } else { - n->metadata = el_strdup("{}"); + n->metadata = el_strdup_persist("{}"); } n->salience = engram_decode_score(salience); n->importance = engram_decode_score(certainty); @@ -6016,7 +6039,7 @@ el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t labe if (!engram_find_layer((uint32_t)lid)) lid = (int64_t)ENGRAM_LAYER_DEFAULT; n->layer_id = (uint32_t)lid; g->node_count++; - return el_wrap_str(el_strdup(n->id)); + return el_wrap_str(el_strdup_persist(n->id)); } /* ── Layer registry public API ────────────────────────────────────────────── @@ -6268,10 +6291,11 @@ void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t EngramEdge* e = &g->edges[g->edge_count]; memset(e, 0, sizeof(*e)); e->id = engram_new_id(); - e->from_id = el_strdup(f); - e->to_id = el_strdup(t); - e->relation = el_strdup(r && *r ? r : "associate"); - e->metadata = el_strdup("{}"); + /* Use el_strdup_persist: edge strings outlive the HTTP request arena. */ + e->from_id = el_strdup_persist(f); + e->to_id = el_strdup_persist(t); + e->relation = el_strdup_persist(r && *r ? r : "associate"); + e->metadata = el_strdup_persist("{}"); e->weight = engram_decode_score(weight); if (e->weight <= 0.0 || e->weight > 1.0) e->weight = 0.5; e->confidence = 1.0; @@ -7080,7 +7104,7 @@ el_val_t engram_load(el_val_t path) { 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 = el_strdup("{}"); } + if (!nn->metadata || !*nn->metadata) { free(nn->metadata); nn->metadata = el_strdup_persist("{}"); } nn->salience = eg_get_num_field(obj, "salience"); nn->importance = eg_get_num_field(obj, "importance"); nn->confidence = eg_get_num_field(obj, "confidence"); @@ -7131,7 +7155,7 @@ el_val_t engram_load(el_val_t path) { ee->to_id = eg_get_str_field(obj, "to_id"); ee->relation = eg_get_str_field(obj, "relation"); ee->metadata = eg_get_str_field(obj, "metadata"); - if (!ee->metadata || !*ee->metadata) { free(ee->metadata); ee->metadata = el_strdup("{}"); } + if (!ee->metadata || !*ee->metadata) { free(ee->metadata); ee->metadata = el_strdup_persist("{}"); } 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"); @@ -8007,13 +8031,13 @@ static int64_t dharma_find_or_create_relation_edge(const char* peer_base, int cr engram_grow_nodes(); EngramNode* n = &g->nodes[g->node_count]; memset(n, 0, sizeof(*n)); - n->id = el_strdup(self_id); - n->content = el_strdup(_el_cgi_dharma_id ? _el_cgi_dharma_id : "(self)"); - n->node_type = el_strdup("DharmaSelf"); - n->label = el_strdup("dharma:self"); - n->tier = el_strdup("Working"); - n->tags = el_strdup("dharma"); - n->metadata = el_strdup("{}"); + n->id = el_strdup_persist(self_id); + n->content = el_strdup_persist(_el_cgi_dharma_id ? _el_cgi_dharma_id : "(self)"); + n->node_type = el_strdup_persist("DharmaSelf"); + n->label = el_strdup_persist("dharma:self"); + n->tier = el_strdup_persist("Working"); + n->tags = el_strdup_persist("dharma"); + n->metadata = el_strdup_persist("{}"); n->salience = 1.0; n->importance = 1.0; n->confidence = 1.0; int64_t now = engram_now_ms(); n->created_at = now; n->updated_at = now; n->last_activated = now; @@ -8024,13 +8048,13 @@ static int64_t dharma_find_or_create_relation_edge(const char* peer_base, int cr engram_grow_nodes(); EngramNode* n = &g->nodes[g->node_count]; memset(n, 0, sizeof(*n)); - n->id = el_strdup(peer_node); - n->content = el_strdup(peer_base); - n->node_type = el_strdup("DharmaPeer"); - n->label = el_strdup(peer_node); - n->tier = el_strdup("Working"); - n->tags = el_strdup("dharma"); - n->metadata = el_strdup("{}"); + n->id = el_strdup_persist(peer_node); + n->content = el_strdup_persist(peer_base); + n->node_type = el_strdup_persist("DharmaPeer"); + n->label = el_strdup_persist(peer_node); + n->tier = el_strdup_persist("Working"); + n->tags = el_strdup_persist("dharma"); + n->metadata = el_strdup_persist("{}"); n->salience = 0.5; n->importance = 0.5; n->confidence = 1.0; int64_t now = engram_now_ms(); n->created_at = now; n->updated_at = now; n->last_activated = now;