self-review 2026-06-26: WM cap, breakthrough floor 0.25→0.10, ISE WM exclusion, /api/neuron/state-events route
Three improvements from today's self-review: 1. ENGRAM_BREAKTHROUGH_WEIGHT 0.25→0.10 Live data showed 524/525 WM nodes at breakthrough floor (0.25). Knowledge nodes promoted at 0.21 decayed to 0.147 in one call, fell below the old 0.25 floor, and were immediately evicted for fresh breakthrough candidates. Natural promotion was invisible. Invariant maintained: 0.10 < all per-type thresholds (min=0.15 Canonical). 2. ENGRAM_WM_CAP=24 with Pass 4 (per-call) + Pass 5 (global) enforcement Without a cap, broad queries like 'knowledge' promote 525+ nodes simultaneously. WM is now bounded to 24 nodes. Algorithm: qsort on promoted weights, keep top-24 by cutoff, evict the rest. Global pass enforces cap across nodes that were promoted in prior calls and persist via working_memory_weight. Validated: WM promoted goes 525→24. Cognitive basis: Cowan (2001) WM ~4 chunks; 24 gives richer multi-topic context while preventing flooding. 3. ISE exclusion from WM + /api/neuron/state-events route InternalStateEvent nodes were reaching WM via breakthrough (5 suppression cycles) because their content (curiosity seed JSON with 'knowledge', 'memory', etc.) triggered lexical seeding. ISEs are observability-only and must never surface in context. Fix: guard in Pass 2 clears suppression_count and skips to wm_weights[i]=0.0. Also added POST /api/neuron/state-events route to server.el (auth-exempt, internal endpoint). The main soul daemon posts ISEs here but the route was missing — all ise_post() calls were silently returning 'not found'. Research: SYNAPSE (arXiv 2601.02744) validates spreading factor 0.8 (our 0.7), top-M WM cap design, and cosine similarity seeding. Next priority: implement cosine similarity initial seeding from the other branch.
This commit is contained in:
BIN
Binary file not shown.
Vendored
+19
-2
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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": "<json-string>"}
|
||||
//
|
||||
// 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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user