diff --git a/engram/dist/engram b/engram/dist/engram index 01ea9d0..0c99cb6 100755 Binary files a/engram/dist/engram and b/engram/dist/engram differ diff --git a/lang/el-compiler/runtime/el_runtime.c b/lang/el-compiler/runtime/el_runtime.c index 6b8cbe7..68e6dca 100644 --- a/lang/el-compiler/runtime/el_runtime.c +++ b/lang/el-compiler/runtime/el_runtime.c @@ -710,6 +710,7 @@ static void jb_init(JsonBuf* b); static void jb_putc(JsonBuf* b, char c); static void jb_puts(JsonBuf* b, const char* s); static void jb_emit_escaped(JsonBuf* b, const char* s); +static char* jb_finish(JsonBuf* b); static int looks_like_string(el_val_t v); static const char* json_find_key(const char* s, const char* key); static const char* json_skip_value(const char* p); @@ -2014,7 +2015,7 @@ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) { jb_puts(&out, ",\"body\":"); jb_emit_escaped(&out, b); jb_putc(&out, '}'); - return el_wrap_str(out.buf); + return el_wrap_str(jb_finish(&out)); } /* ── Filesystem ──────────────────────────────────────────────────────────── */ @@ -2121,7 +2122,7 @@ el_val_t exec_capture(el_val_t cmdv) { char buf[4096]; while (fgets(buf, sizeof(buf), f)) jb_puts(&b, buf); pclose(f); - return el_wrap_str(b.buf); + return el_wrap_str(jb_finish(&b)); } // exec — run a shell command via /bin/sh, capture stdout, return as String. @@ -2145,7 +2146,7 @@ el_val_t exec(el_val_t cmdv) { jb_puts(&b, buf); } pclose(f); - return el_wrap_str(b.buf); + return el_wrap_str(jb_finish(&b)); } // exec_bg — run a shell command in background, return PID as String. @@ -3292,6 +3293,23 @@ static void jb_puts(JsonBuf* b, const char* s) { b->buf[b->len] = '\0'; } +/* jb_finish — call exactly once, at the point a JsonBuf's buffer is handed + * off as an el_val_t return value (via el_wrap_str). JsonBuf allocates through + * raw malloc/realloc (jb_init/jb_reserve), NOT el_strdup/el_strbuf, so unlike + * those helpers it was never registered with the per-request arena — every + * JsonBuf-built JSON string leaked unconditionally, whether or not a request + * arena was active, because nothing ever freed it. el_arena_track() is a + * documented no-op when no arena is active, so this is safe to call from any + * context (HTTP request handler, CLI arena-scope, or no arena at all — the + * last case still leaks, same as before, until the caller's context is itself + * arena-scoped). Do NOT call this on a JsonBuf whose ->buf is consumed by + * another builder (jb_puts(&other, x.buf)) and freed manually — only on the + * one actually returned via el_wrap_str. */ +static char* jb_finish(JsonBuf* b) { + el_arena_track(b->buf); + return b->buf; +} + static void jb_emit_escaped(JsonBuf* b, const char* s) { jb_putc(b, '"'); const unsigned char* p = (const unsigned char*)s; @@ -3400,7 +3418,7 @@ static void jb_emit_value(JsonBuf* b, el_val_t v) { el_val_t json_stringify(el_val_t v) { JsonBuf b; jb_init(&b); jb_emit_value(&b, v); - return el_wrap_str(b.buf); + return el_wrap_str(jb_finish(&b)); } /* ── JSON substring accessors ────────────────────────────────────────────── */ @@ -3587,7 +3605,7 @@ el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value) { jb_putc(&b, ':'); jb_puts(&b, raw_val); jb_putc(&b, '}'); - return el_wrap_str(b.buf); + return el_wrap_str(jb_finish(&b)); } const char* existing = json_find_key(json, k); JsonBuf b; jb_init(&b); @@ -3601,7 +3619,7 @@ el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value) { b.buf[b.len] = '\0'; jb_puts(&b, raw_val); jb_puts(&b, end); - return el_wrap_str(b.buf); + return el_wrap_str(jb_finish(&b)); } /* Insert before closing '}'. Find last '}' */ size_t jl = strlen(json); @@ -3633,7 +3651,7 @@ el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value) { jb_puts(&b, raw_val); /* Append from close_idx onward */ jb_puts(&b, json + close_idx); - return el_wrap_str(b.buf); + return el_wrap_str(jb_finish(&b)); } el_val_t json_array_len(el_val_t json_str) { @@ -3771,7 +3789,7 @@ el_val_t json_build_object(el_val_t kvs) { jb_putc(&b, '"'); } jb_putc(&b, '}'); - return el_wrap_str(b.buf); + return el_wrap_str(jb_finish(&b)); } /* json_build_array — build a JSON array from a list of raw JSON values. @@ -3789,7 +3807,7 @@ el_val_t json_build_array(el_val_t items) { jb_puts(&b, vs); } jb_putc(&b, ']'); - return el_wrap_str(b.buf); + return el_wrap_str(jb_finish(&b)); } /* ── Time ────────────────────────────────────────────────────────────────── */ @@ -5193,7 +5211,7 @@ el_val_t state_keys(void) { } jb_putc(&b, ']'); pthread_mutex_unlock(&_state_mu); - return el_wrap_str(b.buf); + return el_wrap_str(jb_finish(&b)); } /* Returns 1 (true) if the key is present in the state store, else 0 (false). */ @@ -5399,7 +5417,7 @@ el_val_t str_format(el_val_t fmt, el_val_t data) { jb_putc(&b, *p); p++; } - return el_wrap_str(b.buf); + return el_wrap_str(jb_finish(&b)); } el_val_t str_lower(el_val_t s) { return str_to_lower(s); } @@ -5815,7 +5833,7 @@ el_val_t list_join(el_val_t listv, el_val_t sep) { jb_puts(&b, tmp); } } - return el_wrap_str(b.buf); + return el_wrap_str(jb_finish(&b)); } el_val_t list_range(el_val_t start, el_val_t end) { @@ -6465,12 +6483,12 @@ 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"); - n->label = engram_first_n_chars(c, 60); - n->tier = el_strdup("Working"); - n->tags = el_strdup(""); - n->metadata = el_strdup("{}"); + n->content = el_strdup_persist(c ? c : ""); + n->node_type = el_strdup_persist(nt && *nt ? nt : "Memory"); + { char* _lb = engram_first_n_chars(c, 60); n->label = el_strdup_persist(_lb); } /* persist: stored field must outlive request arena */ + 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; @@ -6597,11 +6615,11 @@ 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 : ""); + 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. */ @@ -6610,7 +6628,7 @@ el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t labe 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); @@ -6883,10 +6901,10 @@ 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("{}"); + 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; @@ -7519,11 +7537,11 @@ el_val_t engram_save(el_val_t path) { /* Helper: extract a string field from a JSON object substring. */ static char* eg_get_str_field(const char* obj, const char* key) { const char* p = json_find_key(obj, key); - if (!p) return el_strdup(""); - if (*p != '"') return el_strdup(""); + if (!p) return el_strdup_persist(""); + if (*p != '"') return el_strdup_persist(""); JsonParser jp = { .p = p, .end = p + strlen(p), .err = 0 }; char* out = jp_parse_string_raw(&jp); - if (jp.err) { free(out); return el_strdup(""); } + if (jp.err) { free(out); return el_strdup_persist(""); } return out; } @@ -7597,7 +7615,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"); @@ -7648,7 +7666,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"); @@ -7740,7 +7758,7 @@ el_val_t engram_get_node_json(el_val_t id) { if (!n) return el_wrap_str(el_strdup("{}")); JsonBuf b; jb_init(&b); engram_emit_node_json(&b, n); - return el_wrap_str(b.buf); + return el_wrap_str(jb_finish(&b)); } el_val_t engram_search_json(el_val_t query, el_val_t limit) { @@ -7768,7 +7786,7 @@ el_val_t engram_search_json(el_val_t query, el_val_t limit) { } } jb_putc(&b, ']'); - return el_wrap_str(b.buf); + return el_wrap_str(jb_finish(&b)); } el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset) { @@ -7777,9 +7795,9 @@ el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset) { int64_t off = (int64_t)offset; if (off < 0) off = 0; JsonBuf b; jb_init(&b); jb_putc(&b, '['); - if (g->node_count == 0) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + if (g->node_count == 0) { jb_putc(&b, ']'); return el_wrap_str(jb_finish(&b)); } int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); - if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + if (!idx) { jb_putc(&b, ']'); return el_wrap_str(jb_finish(&b)); } /* Skip transparent layers — introspection filter, same as engram_scan_nodes. */ int64_t live = 0; for (int64_t i = 0; i < g->node_count; i++) { @@ -7797,7 +7815,7 @@ el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset) { } free(idx); jb_putc(&b, ']'); - return el_wrap_str(b.buf); + return el_wrap_str(jb_finish(&b)); } /* engram_scan_nodes_by_type_json — filter by node_type before paginating. @@ -7813,9 +7831,9 @@ el_val_t engram_scan_nodes_by_type_json(el_val_t type_v, el_val_t limit, el_val_ int64_t off = (int64_t)offset; if (off < 0) off = 0; JsonBuf b; jb_init(&b); jb_putc(&b, '['); - if (g->node_count == 0) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + if (g->node_count == 0) { jb_putc(&b, ']'); return el_wrap_str(jb_finish(&b)); } int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t)); - if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + if (!idx) { jb_putc(&b, ']'); return el_wrap_str(jb_finish(&b)); } int64_t live = 0; for (int64_t i = 0; i < g->node_count; i++) { if (engram_layer_is_transparent(g->nodes[i].layer_id)) continue; @@ -7834,7 +7852,7 @@ el_val_t engram_scan_nodes_by_type_json(el_val_t type_v, el_val_t limit, el_val_ } free(idx); jb_putc(&b, ']'); - return el_wrap_str(b.buf); + return el_wrap_str(jb_finish(&b)); } el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction) { @@ -7849,7 +7867,7 @@ el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t di int allow_in = (strcmp(dir, "in") == 0) || (strcmp(dir, "both") == 0); JsonBuf b; jb_init(&b); jb_putc(&b, '['); - if (!sid || !*sid) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + if (!sid || !*sid) { jb_putc(&b, ']'); return el_wrap_str(jb_finish(&b)); } /* Frontier of (node_id, hops). Cap to a sane size. */ char** frontier = calloc(1024, sizeof(char*)); @@ -7859,7 +7877,7 @@ el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t di int64_t vc = 0; if (!frontier || !frontier_h || !visited) { free(frontier); free(frontier_h); free(visited); - jb_putc(&b, ']'); return el_wrap_str(b.buf); + jb_putc(&b, ']'); return el_wrap_str(jb_finish(&b)); } /* Use plain strdup (not el_strdup) so arena doesn't track these pointers. * The BFS loop manually frees them below — arena would double-free them. */ @@ -7902,7 +7920,7 @@ el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t di for (int64_t i = 0; i < vc; i++) free(visited[i]); free(frontier); free(frontier_h); free(visited); jb_putc(&b, ']'); - return el_wrap_str(b.buf); + return el_wrap_str(jb_finish(&b)); } el_val_t engram_activate_json(el_val_t query, el_val_t depth) { @@ -7943,7 +7961,7 @@ el_val_t engram_activate_json(el_val_t query, el_val_t depth) { } } jb_putc(&b, ']'); - return el_wrap_str(b.buf); + return el_wrap_str(jb_finish(&b)); } el_val_t engram_stats_json(void) { @@ -7963,7 +7981,7 @@ el_val_t engram_list_layers_json(void) { jb_putc(&b, '['); /* Build a sorted index over live layers. */ size_t* idx = malloc((g->layer_count + 1) * sizeof(size_t)); - if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); } + if (!idx) { jb_putc(&b, ']'); return el_wrap_str(jb_finish(&b)); } size_t live = 0; for (size_t i = 0; i < g->layer_count; i++) { if (g->layers[i].name) idx[live++] = i; @@ -8000,7 +8018,7 @@ el_val_t engram_list_layers_json(void) { } free(idx); jb_putc(&b, ']'); - return el_wrap_str(b.buf); + return el_wrap_str(jb_finish(&b)); } /* engram_compile_layered_json — produce a prompt-ready context block split @@ -8086,7 +8104,7 @@ el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth) { free(b.buf); return el_wrap_str(el_strdup("")); } - return el_wrap_str(b.buf); + return el_wrap_str(jb_finish(&b)); } /* engram_query_range — temporal range query. @@ -8131,7 +8149,7 @@ el_val_t engram_query_range(el_val_t start_ms_v, el_val_t end_ms_v) { } jb_putc(&b, ']'); free(idx); - return el_wrap_str(b.buf); + return el_wrap_str(jb_finish(&b)); } /* engram_load_merge — like engram_load but WITHOUT resetting the store. @@ -8382,7 +8400,7 @@ el_val_t engram_wm_top_json(el_val_t n_v) { } free(idx); jb_putc(&b, ']'); - return el_wrap_str(b.buf); + return el_wrap_str(jb_finish(&b)); } #ifdef HAVE_CURL @@ -8778,13 +8796,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; @@ -8795,13 +8813,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; @@ -8813,10 +8831,10 @@ static int64_t dharma_find_or_create_relation_edge(const char* peer_base, int cr EngramEdge* e = &g->edges[g->edge_count]; memset(e, 0, sizeof(*e)); e->id = engram_new_id(); - e->from_id = el_strdup(self_id); - e->to_id = el_strdup(peer_node); - e->relation = el_strdup("dharma-relation"); - e->metadata = el_strdup("{}"); + e->from_id = el_strdup_persist(self_id); + e->to_id = el_strdup_persist(peer_node); + e->relation = el_strdup_persist("dharma-relation"); + e->metadata = el_strdup_persist("{}"); e->weight = 0.0; e->confidence = 1.0; int64_t now = engram_now_ms(); @@ -9109,7 +9127,7 @@ static el_val_t llm_extract_text(el_val_t resp_val) { free(obj); p = end; } - return el_wrap_str(out.buf); + return el_wrap_str(jb_finish(&out)); } el_val_t llm_call(el_val_t model, el_val_t prompt) {