diff --git a/lang/el-compiler/runtime/el_runtime.c b/lang/el-compiler/runtime/el_runtime.c index 2f4c839..1036d16 100644 --- a/lang/el-compiler/runtime/el_runtime.c +++ b/lang/el-compiler/runtime/el_runtime.c @@ -6322,7 +6322,9 @@ static void engram_grow_edges(void) { static char* engram_new_id(void) { el_val_t v = uuid_new(); const char* s = EL_CSTR(v); - return el_strdup(s ? s : ""); + /* Persistent: node ids live in the global store; an arena (el_strdup) id is + * freed at el_request_end(), corrupting the node after the creating request. */ + return el_strdup_persist(s ? s : ""); } /* Convert a node into an ElMap of its fields. */ @@ -6417,12 +6419,17 @@ 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("{}"); + /* Persistent (el_strdup_persist, NOT el_strdup): these strings are owned by the + * persistent global node store. el_strdup tracks into the per-request arena, which + * el_request_end() frees when the creating HTTP request completes — leaving the + * stored node with dangling pointers (corrupted ids, "saved but never listed"). + * This is the root cause of the hallucinated/lost-saves class of bugs. */ + 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); @@ -7365,13 +7372,28 @@ el_val_t engram_save(el_val_t path) { jb_putc(&b, '}'); } jb_puts(&b, "]}"); - FILE* f = fopen(p, "wb"); - if (!f) { free(b.buf); return 0; } + { + struct stat _st; + if (stat(p, &_st) == 0 && _st.st_size > 200000 && + (uint64_t)b.len < (uint64_t)_st.st_size / 16) { + fprintf(stderr, "[engram_save] REFUSED sparse write: new %zu vs existing %lld (<1/16) protecting %s\n", + b.len, (long long)_st.st_size, p); + free(b.buf); return 0; + } + } + size_t _plen = strlen(p); + char* _tmp = (char*)malloc(_plen + 5); + if (!_tmp) { free(b.buf); return 0; } + memcpy(_tmp, p, _plen); memcpy(_tmp + _plen, ".tmp", 5); + FILE* f = fopen(_tmp, "wb"); + if (!f) { free(_tmp); free(b.buf); return 0; } size_t w = fwrite(b.buf, 1, b.len, f); - fclose(f); - int ok = (w == b.len); - free(b.buf); - return ok ? 1 : 0; + int wok = (w == b.len); + if (wok) { fflush(f); fsync(fileno(f)); } + fclose(f); free(b.buf); + if (!wok) { unlink(_tmp); free(_tmp); return 0; } + if (rename(_tmp, p) != 0) { unlink(_tmp); free(_tmp); return 0; } + free(_tmp); return 1; } /* Helper: extract a string field from a JSON object substring. */