chore: integrate local main commits #63
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