diff --git a/lang/runtime/el_runtime.c b/lang/runtime/el_runtime.c index 10c1233..8abc646 100644 --- a/lang/runtime/el_runtime.c +++ b/lang/runtime/el_runtime.c @@ -5168,10 +5168,23 @@ el_val_t state_get(el_val_t key) { if (!k) return el_wrap_str(el_strdup("")); pthread_mutex_lock(&_state_mu); StateEntry* e = state_find(k); - char* result = el_strdup_persist(e ? e->value : ""); + /* ONE arena-tracked copy, taken under the lock. + * + * This used to make TWO copies: an el_strdup_persist temporary, then an + * arena-tracked copy of that temporary. The persistent one was never + * returned and never freed — el_strdup_persist bypasses the arena by + * design ("state_set, engram internals"), so arena-pop could not reclaim + * it. Every state_get therefore leaked its full value string, permanently. + * + * The soul's awareness loop has 68 state_get call sites and ticks every + * 200ms; measured leak was ~1.1 MB per tick, about 19 GB/hour. It went + * unnoticed for as long as the soul restarted often enough to mask it. + * + * el_strdup tracks into the thread-local arena, which touches no shared + * state, so doing it under _state_mu is safe and removes the need for the + * temporary entirely. */ + char* copy = el_strdup(e ? e->value : ""); pthread_mutex_unlock(&_state_mu); - /* wrap in arena-tracked copy for the caller's request lifetime */ - char* copy = el_strdup(result); return el_wrap_str(copy); }