Compare commits

..

1 Commits

Author SHA1 Message Date
Tim Lingo 2dec76c87a fix(runtime): reconcile live data-integrity fixes onto main (UAF + atomic engram_save)
El SDK Release / build-and-release (pull_request) Failing after 17s
Ports the fixes that until now lived only in the un-versioned el-sdk source the live
macOS soul was hand-built from (captured in the [DO NOT MERGE] live-darwin-runtime
snapshot) FORWARD onto main, faithfully and minimally — without dragging in the
snapshot's deletions of main's newer engram_wm_/engram_load_merge/http_serve_async.

1. UAF (hallucinated/lost-saves root cause): engram_new_id + engram_node_full now use
   el_strdup_persist, NOT el_strdup. el_strdup tracks into the per-request arena that
   el_request_end() frees when the creating HTTP request completes — leaving stored
   nodes with dangling pointers (corrupted ids, 'saved but never listed'). Transplanted
   verbatim from the live runtime; el_strdup_persist sites 19->27, matching live.

2. Atomic engram_save: write <path>.tmp, fflush+fsync, rename() over target (atomic on
   POSIX) so a booting soul's engram_load never reads a truncated/0-byte snapshot — the
   genesis -> nodes=1 -> 63-node-clobber loop. Plus a sparse-write floor: refuse to
   overwrite a >200KB snapshot with one < 1/16 its size. (Validated in isolation:
   harness 11/11; rebuilt+booted the darwin soul, round-tripped 5113 nodes, no clobber.)

The response-truncation fix is already on main (_tl_fs_read_len binary-safe length).
Compiles clean. For Will to build through CI/elb and deploy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 19:46:56 -05:00
+37 -37
View File
@@ -6031,14 +6031,6 @@ void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
#define ENGRAM_LAYER_DOMAIN 2u
#define ENGRAM_LAYER_IMPRINT 3u
#define ENGRAM_LAYER_SUIT 4u
#define ENGRAM_LAYER_ACCUMULATION 5u
/* New user-facing nodes (memories, knowledge, conversations) are created in the
* accumulation layer the top of the consciousness stack, the engram the user
* sees; every layer below shapes behavior but is hidden from the user (Layered
* Consciousness architecture, app 64/064,262). ENGRAM_LAYER_DEFAULT stays
* core-identity ON PURPOSE: it is the fallback home for LEGACY nodes loaded from
* snapshots without a layer_id, so existing data (the originator corpus) is
* never migrated out of its established layer. New != legacy. */
#define ENGRAM_LAYER_DEFAULT ENGRAM_LAYER_CORE_IDENTITY
/* Pass 3 override floor. Layer 0 nodes that received any background
@@ -6216,20 +6208,6 @@ static void engram_init_layers(EngramStore* g) {
.transparent = 0,
.injectable = 1
};
/* Layer 5 — accumulation. The TOP of the consciousness stack: the default
* home for all new user-facing nodes. This is the engram the user sees;
* every layer below shapes behavior but is hidden from the user. Not
* injectable it is the persistent user accumulation, not a swappable
* overlay. transparent=0: its content is surfaced to introspection (it is
* the user's own knowledge/memory), unlike the lower behavioral layers. */
g->layers[g->layer_count++] = (EngramLayer){
.layer_id = ENGRAM_LAYER_ACCUMULATION,
.name = el_strdup_persist("accumulation"),
.activation_priority = 50,
.suppressible = 1,
.transparent = 0,
.injectable = 0
};
}
static EngramStore* engram_get(void) {
@@ -6344,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. */
@@ -6421,7 +6401,7 @@ el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience) {
n->last_activated = now;
n->created_at = now;
n->updated_at = now;
n->layer_id = ENGRAM_LAYER_ACCUMULATION; /* new user-facing node → top layer */
n->layer_id = ENGRAM_LAYER_DEFAULT;
g->node_count++;
return el_wrap_str(el_strdup(n->id));
}
@@ -6439,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);
@@ -6457,7 +6442,7 @@ el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label,
n->last_activated = now;
n->created_at = now;
n->updated_at = now;
n->layer_id = ENGRAM_LAYER_ACCUMULATION; /* new user-facing node → top layer */
n->layer_id = ENGRAM_LAYER_DEFAULT;
g->node_count++;
return el_wrap_str(el_strdup(n->id));
}
@@ -7387,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. */