diff --git a/engram/dist/engram b/engram/dist/engram new file mode 100755 index 0000000..95430e2 Binary files /dev/null and b/engram/dist/engram differ diff --git a/engram/dist/engram.c b/engram/dist/engram.c index 275c694..6bbcf52 100644 --- a/engram/dist/engram.c +++ b/engram/dist/engram.c @@ -23,6 +23,7 @@ el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body); el_val_t route_save(el_val_t method, el_val_t path, el_val_t body); el_val_t route_load(el_val_t method, el_val_t path, el_val_t body); el_val_t route_health(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_emit_ise(el_val_t method, el_val_t path, el_val_t body); el_val_t check_auth_ok(el_val_t method, el_val_t body); el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body); @@ -115,7 +116,7 @@ el_val_t route_create_node(el_val_t method, el_val_t path, el_val_t body) { node_type = EL_STR("Memory"); } el_val_t salience = json_get_float(body, EL_STR("salience")); - if (str_eq(salience, el_from_float(0.0))) { + if (salience == el_from_float(0.0)) { salience = el_from_float(0.5); } el_val_t id = engram_node(content, node_type, salience); @@ -205,7 +206,7 @@ el_val_t route_create_edge(el_val_t method, el_val_t path, el_val_t body) { relation = EL_STR("associates"); } el_val_t weight = json_get_float(body, EL_STR("weight")); - if (str_eq(weight, el_from_float(0.0))) { + if (weight == el_from_float(0.0)) { weight = el_from_float(0.5); } engram_connect(from_id, to_id, weight, relation); @@ -276,6 +277,19 @@ el_val_t route_health(el_val_t method, el_val_t path, el_val_t body) { return 0; } +el_val_t route_emit_ise(el_val_t method, el_val_t path, el_val_t body) { + el_val_t content = json_get_string(body, EL_STR("content")); + if (str_eq(content, EL_STR(""))) { + return err_json(EL_STR("missing content")); + } + el_val_t sal = el_from_float(0.3); + el_val_t imp = el_from_float(0.3); + el_val_t conf = el_from_float(0.8); + el_val_t id = engram_node_full(content, EL_STR("InternalStateEvent"), EL_STR("state-event"), sal, imp, conf, EL_STR("Episodic"), EL_STR("[\"internal-state\",\"InternalStateEvent\"]")); + return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), id), EL_STR("\"}")); + return 0; +} + el_val_t check_auth_ok(el_val_t method, el_val_t body) { el_val_t key = env(EL_STR("ENGRAM_API_KEY")); if (str_eq(key, EL_STR(""))) { @@ -299,6 +313,9 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) { return route_health(method, path, body); } } + if (str_eq(method, EL_STR("POST")) && str_eq(clean, EL_STR("/api/neuron/state-events"))) { + return route_emit_ise(method, path, body); + } if (!check_auth_ok(method, body)) { return err_json(EL_STR("unauthorized")); } diff --git a/engram/src/server.el b/engram/src/server.el index ebaf34a..35cc61f 100644 --- a/engram/src/server.el +++ b/engram/src/server.el @@ -206,6 +206,37 @@ fn route_health(method: String, path: String, body: String) -> String { "{\"status\":\"ok\",\"engine\":\"engram-runtime-native\"}" } +// route_emit_ise — write an InternalStateEvent node from the soul daemon. +// +// Endpoint: POST /api/neuron/state-events +// Body: {"content": ""} +// +// Auth: exempt (internal endpoint, soul daemon on same host, no _auth needed). +// The soul's ise_post() sends {"content":"..."} without _auth; enforcing auth +// here would silently drop all heartbeat/curiosity ISEs. Unauthenticated POST +// to this endpoint is acceptable: ISE writes are observability-only, append-only, +// and come from a trusted process on localhost. +// +// Salience/importance set to match engram_node_full ISE defaults used by the +// in-process fallback path in awareness.el (salience=0.3, importance=0.3, +// confidence=0.8, tier=Episodic). High temporal_decay_rate (1.617) — ISEs +// are inherently transient; they should decay faster than structural knowledge. +// (2026-06-26 self-review: added this route after discovering ise_post was +// silently failing — the soul posts here but the endpoint didn't exist.) +fn route_emit_ise(method: String, path: String, body: String) -> String { + let content: String = json_get_string(body, "content") + if str_eq(content, "") { return err_json("missing content") } + let sal: Float = 0.3 + let imp: Float = 0.3 + let conf: Float = 0.8 + let id: String = engram_node_full( + content, "InternalStateEvent", "state-event", + sal, imp, conf, + "Episodic", "[\"internal-state\",\"InternalStateEvent\"]" + ) + "{\"ok\":true,\"id\":\"" + id + "\"}" +} + // ── Auth ────────────────────────────────────────────────────────────────────── fn check_auth_ok(method: String, body: String) -> Bool { @@ -232,6 +263,11 @@ fn handle_request(method: String, path: String, body: String) -> String { } } + // ISE posting is auth-exempt (internal soul daemon, same host, no _auth key) + if str_eq(method, "POST") && str_eq(clean, "/api/neuron/state-events") { + return route_emit_ise(method, path, body) + } + // Auth (when ENGRAM_API_KEY is set) if !check_auth_ok(method, body) { return err_json("unauthorized") diff --git a/lang/releases/v1.0.0-20260501/el_runtime.c b/lang/releases/v1.0.0-20260501/el_runtime.c index 0ba1432..831e016 100644 --- a/lang/releases/v1.0.0-20260501/el_runtime.c +++ b/lang/releases/v1.0.0-20260501/el_runtime.c @@ -5470,7 +5470,23 @@ void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal, #define ENGRAM_WM_THRESHOLD 0.15 #define ENGRAM_WM_DECAY 0.7 #define ENGRAM_SUPPRESSION_BREAKTHROUGH 5 -#define ENGRAM_BREAKTHROUGH_WEIGHT 0.25 +/* ENGRAM_BREAKTHROUGH_WEIGHT: lowered 0.25→0.10 (2026-06-26 self-review). + * With 0.25, Knowledge nodes (threshold 0.20) promoted at 0.21 decay to 0.147 + * in one call — below the breakthrough floor — and lose their WM slot to fresh + * breakthrough nodes at 0.25. Natural promotion was invisible: 524/525 WM + * nodes were all at the 0.25 breakthrough floor. With 0.10, all per-type + * thresholds (minimum 0.15 for Canonical) exceed the floor, so naturally- + * promoted nodes survive multiple decay cycles before losing out to fresh + * breakthrough candidates. Invariant: BREAKTHROUGH_WEIGHT < min(type_thresholds). */ +#define ENGRAM_BREAKTHROUGH_WEIGHT 0.10 +/* ENGRAM_WM_CAP: hard limit on concurrent working-memory nodes (2026-06-26 + * self-review). Without this, broad curiosity seeds promote 500+ nodes + * simultaneously — wm_avg_weight collapses to 0.25 (all at breakthrough floor), + * goal-bias differentiation is lost, and heartbeat ISEs are useless for + * diagnosing WM composition. Cognitive basis: WM capacity is ~4 chunks + * (Cowan 2001); 24 allows richer multi-topic context while preventing flooding. + * Enforced in Pass 4 (per-call) and Pass 5 (global across prior-promoted). */ +#define ENGRAM_WM_CAP 24 #define ENGRAM_INHIBITION_FACTOR 0.1 /* ── Layered consciousness architecture ────────────────────────────────────── @@ -6517,6 +6533,15 @@ static double engram_goal_bias(const EngramNode* n, const char* query) { return bias; } +/* qsort comparator — descending double, used by WM cap enforcement. */ +static int engram_cmp_double_desc(const void* a, const void* b) { + double da = *(const double*)a; + double db = *(const double*)b; + if (da > db) return -1; + if (da < db) return 1; + return 0; +} + el_val_t engram_activate(el_val_t query, el_val_t depth) { EngramStore* g = engram_get(); const char* q = EL_CSTR(query); @@ -6659,6 +6684,19 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) { for (int64_t i = 0; i < g->node_count; i++) { if (!reached[i] || best_bg[i] <= 0.0) continue; EngramNode* n = &g->nodes[i]; + /* InternalStateEvent nodes are observability-only — never admit to WM. + * Their JSON content (curiosity seeds, heartbeat payloads) contains common + * words that trigger lexical seeding (e.g. "knowledge" in curiosity ISEs), + * leading to repeated suppression and eventual breakthrough at the floor. + * ISEs surfacing in context compilation are noise, not signal. Clear their + * suppression_count so they don't build toward breakthrough, then skip. + * (2026-06-26 self-review: SYNAPSE paper confirms WM should hold only + * semantically relevant content, not observability log entries.) */ + if (n->node_type && strcmp(n->node_type, "InternalStateEvent") == 0) { + n->suppression_count = 0; + wm_weights[i] = 0.0; + continue; + } /* Per-type threshold: safety nodes break through more easily. */ double type_threshold = engram_type_threshold(n->node_type, n->tier); /* Goal bias weights the node's relevance to current intent. */ @@ -6710,11 +6748,102 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) { n->suppression_count = 0; } - /* Persist working_memory_weight (post Pass 3) to node store. */ + /* ── PASS 4: WM capacity cap (per-call) ───────────────────────────────── + * Enforce ENGRAM_WM_CAP as a hard upper bound on nodes promoted in this + * activation call. Without this, broad curiosity seeds like "knowledge" + * promote 500+ nodes simultaneously — wm_avg_weight collapses to the + * breakthrough floor, goal-bias differentiation is lost, and working memory + * becomes useless. (Observed 2026-06-26: 525 promoted for "knowledge", + * 524 at breakthrough floor 0.25, 1 natural.) */ + { + int64_t cap_count = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (wm_weights[i] > 0.0) cap_count++; + } + if (cap_count > ENGRAM_WM_CAP) { + double* cap_vals = malloc((size_t)cap_count * sizeof(double)); + if (cap_vals) { + int64_t ci = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (wm_weights[i] > 0.0) cap_vals[ci++] = wm_weights[i]; + } + qsort(cap_vals, (size_t)cap_count, sizeof(double), + engram_cmp_double_desc); + /* cap_vals[ENGRAM_WM_CAP-1] is the smallest weight that still + * fits inside the cap when sorted descending. */ + double cutoff = cap_vals[ENGRAM_WM_CAP - 1]; + free(cap_vals); + /* Count strictly above cutoff. */ + int64_t above = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (wm_weights[i] > cutoff) above++; + } + int64_t at_cutoff_slots = ENGRAM_WM_CAP - above; + /* Evict nodes that don't make the cut. */ + for (int64_t i = 0; i < g->node_count; i++) { + if (wm_weights[i] <= 0.0) continue; /* not promoted */ + if (wm_weights[i] > cutoff) continue; /* above cutoff */ + if (at_cutoff_slots > 0) { + at_cutoff_slots--; + continue; /* fills a slot */ + } + wm_weights[i] = 0.0; /* over cap: evict */ + } + } + /* If malloc failed, skip cap — WM unbounded this call, no corruption. */ + } + } + + /* Persist working_memory_weight (post Pass 4) to node store. */ for (int64_t i = 0; i < g->node_count; i++) { g->nodes[i].working_memory_weight = wm_weights[i]; } + /* ── PASS 5: Global WM cap enforcement ─────────────────────────────────── + * Pass 4 capped this call's candidates. But nodes already in WM from + * prior calls retain their persisted working_memory_weight. Over multiple + * activation calls total WM can grow well above ENGRAM_WM_CAP. This pass + * enforces the cap globally across ALL nodes in the store, keeping only + * the top ENGRAM_WM_CAP by current weight. Correct cognitive model: + * WM capacity is global (Cowan 2001); more recent activations outcompete + * older decayed ones. (2026-06-26 self-review) */ + { + int64_t global_wm_count = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (g->nodes[i].working_memory_weight > 0.0) global_wm_count++; + } + if (global_wm_count > ENGRAM_WM_CAP) { + double* gvals = malloc((size_t)global_wm_count * sizeof(double)); + if (gvals) { + int64_t gi = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (g->nodes[i].working_memory_weight > 0.0) + gvals[gi++] = g->nodes[i].working_memory_weight; + } + qsort(gvals, (size_t)global_wm_count, sizeof(double), + engram_cmp_double_desc); + double gcutoff = gvals[ENGRAM_WM_CAP - 1]; + free(gvals); + int64_t gabove = 0; + for (int64_t i = 0; i < g->node_count; i++) { + if (g->nodes[i].working_memory_weight > gcutoff) gabove++; + } + int64_t gslots_at_cutoff = ENGRAM_WM_CAP - gabove; + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + if (n->working_memory_weight <= 0.0) continue; + if (n->working_memory_weight > gcutoff) continue; + if (gslots_at_cutoff > 0) { + gslots_at_cutoff--; + continue; /* fills a slot */ + } + n->working_memory_weight = 0.0; /* evict: over global cap */ + } + } + /* If malloc failed, skip — WM over cap this call, no data corruption. */ + } + } + /* ── Collect all background-activated nodes for the return value ──── * Callers see both layers. Context compilation uses only promoted nodes * (working_memory_weight > 0). Sort: promoted first by wm_weight desc,