Compare commits

...

4 Commits

Author SHA1 Message Date
Neuron c79033b749 runtime: let signal enter as geometry, not as prose about signal
El SDK CI - dev / build-and-test (pull_request) Failing after 10m55s
No ingest path could carry a vector. engram_node/_full/_layered take text
only, and a node acquired an embedding solely via engram_embed_backfill
DERIVING one from n->content. That made text the mandatory entry medium:
any non-text modality had to be described in prose first, so the geometry
we then reasoned over was the geometry OF THE DESCRIPTION, not of the
signal. Measured: POST /api/nodes accepted an "emb" field, returned 200
with a fresh id, and stored nothing — emb_dim=None, embedded=false.

engram_node_set_emb attaches a vector to an existing node. Off-dimension
vectors are stored but not indexed (the HNSW build loop already filters on
emb_dim), so modality geometry is durable and addressable without
perturbing the canonical index. Setting emb also makes the node ineligible
for embed_backfill, so a realizer's vector is never overwritten by a
text-derived one.

Two reporting fixes ride along, because both are how the drop stayed
invisible: the create response now reports emb_set instead of being
success-shaped regardless, and the node document now always emits emb_dim
and embedded — without which a genuine ingest drop and a mere reporting
gap are indistinguishable.

Verified live: voice node emb_dim=64 embedded=true; text control emb_dim=0
embedded=false; malformed hex, length mismatch and dim<=0 all reject.

KNOWN PLACEMENT DEFECT: this is at the consumer. Ingest is a language
concern, not an engram feature — every el program touching any modality
needs it. The vector also marshals as a hex STRING because el has no
first-class geometry value, which reintroduces text as the transport
medium one layer below the problem being fixed. The durable shape is
geometry as an el value plus declarable realizers, after which the engram
stops having an ingest concept at all. Landing this as the verified probe
that proves the path.
2026-08-16 11:12:48 -05:00
will.anderson 1119295238 Merge pull request 'runtime: state_get leaked its value on every call' (#140) from fix/state-get-leak into dev
El SDK CI - dev / build-and-test (push) Failing after 14m5s
2026-08-16 13:09:58 +00:00
bigmerge 9c07970943 runtime: state_get leaked its value on every call
El SDK CI - dev / build-and-test (pull_request) Failing after 14m26s
char* result = el_strdup_persist(e ? e->value : "");   // never freed
    pthread_mutex_unlock(&_state_mu);
    char* copy = el_strdup(result);                        // arena-tracked
    return el_wrap_str(copy);

Two copies were made. `result` existed only as the source for `copy` — never
returned, never freed — and el_strdup_persist bypasses the arena BY DESIGN
("state_set, engram internals"), so arena-pop could never reclaim it. Every
state_get leaked its full value string, permanently.

MEASURED: 200,000 state_get calls against a 64-byte value.
    before   15 MB peak RSS growth   (~75 bytes/call — the value plus overhead)
    after     0 MB

IMPACT. The soul's awareness loop has 68 state_get call sites and ticks every
200ms. Live measurement before the fix: RSS climbing 112 MB per 20s, about
19 GB/hour, in awareness_run -> one_cycle -> perceive, while node_count stayed
flat at ~13,479 — growth with no data behind it. It drove the host from 20 GB
free to 4.3 GB in roughly an hour.

WHY NOW, since the code is old: the soul used to restart constantly (no
write-through, divergent graph, 2.11 GB). Stabilising it (neuron #162) let it
stay up long enough to accumulate. The fix did not cause this leak; it removed
the crashes that were hiding it. Same pattern as the test framework surfacing
math_log — the defect was always there, something finally made it visible.

Found by Ishikawa rather than by reading the nearest code: method (arena
push/pop IS correctly paired per tick), material (node count flat, so not data
growth), environment (19 GB/hr / 18,000 ticks = ~1.1 MB per tick, so per-tick
not one-shot), machine (an allocator that bypasses the arena) — which is where
the evidence pointed.

el_strdup tracks into the thread-local arena, which touches no shared state, so
taking the single copy under _state_mu is safe and removes the temporary
entirely.

Verified: self-hosting fixpoint byte-identical; state round-trip correct for
hit, miss, and overwrite.
2026-08-16 08:09:32 -05:00
will.anderson 0832865952 Merge pull request 'test framework phase 3/4: black_box barrier + three-signal complexity gate, armed' (#139) from wt/soul-runtime-reconcile into dev
El SDK CI - dev / build-and-test (push) Failing after 11m32s
2026-08-16 03:02:18 +00:00
3 changed files with 130 additions and 4 deletions
+26 -1
View File
@@ -288,6 +288,27 @@ fn route_create_node(method: String, path: String, body: String) -> String {
salience, importance, confidence, salience, importance, confidence,
tier, tags tier, tags
) )
// GEOMETRY INGEST (2026-08-16 self-review): this route accepted an "emb"
// field, returned 200 with a fresh id, and stored NOTHING engram_node_full
// has no vector parameter, so the caller's geometry was silently discarded
// and the node came back emb_dim=None / embedded:false. Measured live while
// trying to admit a voice signal. The consequence was structural, not
// cosmetic: text was the only entry medium, so any non-text modality had to
// be DESCRIBED in prose and what we then reasoned over was the geometry of
// the description, not of the signal.
//
// "emb" is little-endian float32 hex (dim*8 chars) the encoding the
// perception vessel's /voice/embed already emits, so a realizer's output
// moves in with no float-array round trip. "dim" defaults to the vector's
// implied width. Off-dimension vectors are stored but not inserted into the
// resident index (its build loop filters on emb_dim), so a modality vector
// is durable and addressable without perturbing the canonical index.
let emb_hex: String = json_get_string(body, "emb")
let emb_set: Int = if str_eq(emb_hex, "") { 0 } else {
let dim_raw: String = json_get_raw(body, "dim")
let dim: Int = if str_eq(dim_raw, "") { str_len(emb_hex) / 8 } else { json_get_int(body, "dim") }
engram_node_set_emb(id, emb_hex, dim)
}
let saved: Int = persist_node(id) let saved: Int = persist_node(id)
// ORPHAN PREVENTION (ENGRAM_AUTOCONNECT): connect the fresh node to its // ORPHAN PREVENTION (ENGRAM_AUTOCONNECT): connect the fresh node to its
// nearest embedded neighbors so it never enters the graph edgeless. // nearest embedded neighbors so it never enters the graph edgeless.
@@ -298,7 +319,11 @@ fn route_create_node(method: String, path: String, body: String) -> String {
if added > 0 { let sv2: Int = persist_edges_since(ec0) } if added > 0 { let sv2: Int = persist_edges_since(ec0) }
added added
} else { 0 } } else { 0 }
"{\"id\":\"" + id + "\",\"content\":\"" + content + "\",\"node_type\":\"" + node_type + "\",\"connected\":" + int_to_str(connected) + "}" // Report whether the supplied geometry actually landed. The old response
// was success-shaped regardless 200 with an id while the vector was
// discarded which is how the drop went unnoticed. A caller can now
// assert on emb_set instead of trusting the status code.
"{\"id\":\"" + id + "\",\"content\":\"" + content + "\",\"node_type\":\"" + node_type + "\",\"connected\":" + int_to_str(connected) + ",\"emb_set\":" + int_to_str(emb_set) + "}"
} }
fn route_get_node(method: String, path: String, body: String) -> String { fn route_get_node(method: String, path: String, body: String) -> String {
+99 -3
View File
@@ -5168,10 +5168,23 @@ el_val_t state_get(el_val_t key) {
if (!k) return el_wrap_str(el_strdup("")); if (!k) return el_wrap_str(el_strdup(""));
pthread_mutex_lock(&_state_mu); pthread_mutex_lock(&_state_mu);
StateEntry* e = state_find(k); 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); 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); return el_wrap_str(copy);
} }
@@ -8494,6 +8507,80 @@ el_val_t engram_node_count(void) {
return (el_val_t)engram_get()->node_count; return (el_val_t)engram_get()->node_count;
} }
/* engram_node_set_emb — attach GEOMETRY to an existing node.
*
* WHY THIS EXISTS (2026-08-16). Until now no ingest path could carry a
* vector. engram_node / engram_node_full / engram_node_layered take text
* only, and the sole way a node acquired an embedding was
* engram_embed_backfill DERIVING one from n->content. That made text the
* mandatory entry medium: any non-text modality (audio, image, sensor)
* had to be described in prose first, and the geometry we then reasoned
* over was the geometry OF THE DESCRIPTION, not of the signal. Measured
* consequence: POST /api/nodes accepted an "emb" field, returned 200 with
* a fresh id, and stored emb_dim=None / embedded:false the vector was
* silently discarded because no parameter existed to receive it.
*
* `hex` is little-endian float32, the encoding the perception vessel's
* /voice/embed already emits, so a realizer's output moves in without a
* JSON float-array round trip. Length must be exactly dim*8 hex chars.
*
* DIMENSION POLICY: dim need NOT equal the canonical text-embedding dim.
* A modality vector of a different width is stored and is simply not
* inserted into the resident HNSW index, whose build loop already filters
* on `n->emb_dim == dim`. So off-dimension geometry is durable and
* addressable without perturbing the canonical index.
*
* Setting emb also makes the node ineligible for embed_backfill (which
* only fills nodes with no emb), so a realizer's vector is never
* overwritten by a text-derived one.
*
* Returns 1 on success, 0 on unknown id / malformed hex / bad dim. */
el_val_t engram_node_set_emb(el_val_t id, el_val_t hex, el_val_t dim) {
const char* sid = EL_CSTR(id);
const char* sh = EL_CSTR(hex);
int32_t d = (int32_t)(int64_t)dim;
/* Bound the allocation. No max-dim constant existed because no caller
* could supply a dim before this function; 8192 is generous for any
* realizer (canonical text embeddings are 768, MFCC voice stats 64)
* while keeping a malformed `dim` from requesting an unbounded malloc. */
if (!sid || !*sid || !sh || d <= 0 || d > 8192) return (el_val_t)0;
size_t need = (size_t)d * 8u; /* 4 bytes → 8 hex chars per float */
if (strlen(sh) != need) return (el_val_t)0;
EngramNode* n = engram_find_node(sid);
if (!n) return (el_val_t)0;
float* v = (float*)malloc(sizeof(float) * (size_t)d);
if (!v) return (el_val_t)0;
for (int32_t i = 0; i < d; i++) {
uint32_t w = 0;
for (int k = 0; k < 8; k++) {
char c = sh[(size_t)i * 8u + (size_t)k];
uint32_t nib;
if (c >= '0' && c <= '9') nib = (uint32_t)(c - '0');
else if (c >= 'a' && c <= 'f') nib = (uint32_t)(c - 'a' + 10);
else if (c >= 'A' && c <= 'F') nib = (uint32_t)(c - 'A' + 10);
else { free(v); return (el_val_t)0; }
w = (w << 4) | nib;
}
/* Hex is emitted little-endian byte order; rebuild the word. */
uint32_t le = ((w & 0x000000FFu) << 24) | ((w & 0x0000FF00u) << 8) |
((w & 0x00FF0000u) >> 8) | ((w & 0xFF000000u) >> 24);
float f;
memcpy(&f, &le, sizeof(f));
v[i] = f;
}
free(n->emb);
n->emb = v;
n->emb_dim = d;
n->updated_at = engram_now_ms();
if (engram_store_enabled()) eg_store_put_node(n);
return (el_val_t)1;
}
/* ── Telemetry retention ──────────────────────────────────────────────────── /* ── Telemetry retention ────────────────────────────────────────────────────
* (2026-07-16 self-review) InternalStateEvent nodes are append-only telemetry * (2026-07-16 self-review) InternalStateEvent nodes are append-only telemetry
* (heartbeat, curiosity_scan, engram_sync) written ~3/min by the awareness * (heartbeat, curiosity_scan, engram_sync) written ~3/min by the awareness
@@ -11289,6 +11376,15 @@ static void engram_emit_node_json(JsonBuf* b, const EngramNode* n, int include_e
snprintf(tmp, sizeof(tmp), ",\"wm_anchor\":%g", n->wm_anchor); jb_puts(b, tmp); snprintf(tmp, sizeof(tmp), ",\"wm_anchor\":%g", n->wm_anchor); jb_puts(b, tmp);
snprintf(tmp, sizeof(tmp), ",\"base_level\":%g", snprintf(tmp, sizeof(tmp), ",\"base_level\":%g",
engram_bll_base_level(n, engram_now_ms())); jb_puts(b, tmp); engram_bll_base_level(n, engram_now_ms())); jb_puts(b, tmp);
/* GEOMETRY VISIBILITY (2026-08-16 self-review): the node document never
* said whether the node carried a vector, so a read-back could not tell
* "has geometry" from "text only". Not cosmetic it is exactly how a
* real ingest drop and a mere reporting gap became indistinguishable,
* and I misdiagnosed one as the other for an hour. Always emit the width
* and the boolean; the vector itself stays behind include_emb since it
* is large and most callers do not want it inline. */
snprintf(tmp, sizeof(tmp), ",\"emb_dim\":%d,\"embedded\":%s",
(int)n->emb_dim, (n->emb && n->emb_dim > 0) ? "true" : "false"); jb_puts(b, tmp);
/* Base-level access history: chronological (oldest→newest) compact /* Base-level access history: chronological (oldest→newest) compact
* string. Loaders replay it through engram_bll_record_access; absent * string. Loaders replay it through engram_bll_record_access; absent
* field = empty ring (optimized-form fallback). (2026-07-22) */ * field = empty ring (optimized-form fallback). (2026-07-22) */
+5
View File
@@ -613,6 +613,11 @@ void engram_strengthen(el_val_t node_id);
void engram_forget(el_val_t node_id); void engram_forget(el_val_t node_id);
el_val_t engram_prune_telemetry(el_val_t older_than_ms); el_val_t engram_prune_telemetry(el_val_t older_than_ms);
el_val_t engram_node_count(void); el_val_t engram_node_count(void);
/* Attach geometry to an existing node. `hex` is little-endian float32,
* exactly dim*8 hex chars — the encoding realizers already emit. Lets a
* non-text modality enter as geometry instead of being described in prose
* and embedded as its description. Returns 1 on success, 0 otherwise. */
el_val_t engram_node_set_emb(el_val_t id, el_val_t hex, el_val_t dim);
el_val_t engram_search(el_val_t query, el_val_t limit); el_val_t engram_search(el_val_t query, el_val_t limit);
el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset); el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset);
void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation); void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation);