diff --git a/engram/test/run_interoception_p4.sh b/engram/test/run_interoception_p4.sh new file mode 100755 index 0000000..9d81bc4 --- /dev/null +++ b/engram/test/run_interoception_p4.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# M-INTEROCEPTION P4 gate: afferent input counters in act-stats (additive). +set -u +HERE="$(cd "$(dirname "$0")" && pwd)" +RT="$HERE/../../lang/runtime/el_runtime.c" +ST="$HERE/../../lang/runtime/engram_store.c" +GEO="$HERE/../../lang/runtime/engram_geometry.c" +VIDX="$HERE/../../lang/runtime/engram_vindex.c" +INC="$HERE/../../lang/runtime" +WORK="$(mktemp -d /tmp/engram-p4-XXXXXX)" +export HOME="$WORK/home"; mkdir -p "$HOME" +unset ENGRAM_STORE +fail=0 + +echo "== compile ==" +gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p4_afferent.c" "$RT" "$ST" "$GEO" "$VIDX" \ + -lcurl -lm -o "$WORK/p4" 2>"$WORK/cc.log" || { echo "COMPILE FAILED"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; } + +"$WORK/p4" > "$WORK/out.txt" 2>&1 || { echo "FAIL run"; cat "$WORK/out.txt"; fail=1; } +grep -oE 'aff_[a-z_]+":[0-9]+' "$WORK/out.txt" | sed 's/^/ /' | head -30 + +echo +echo "== assertions ==" +python3 - "$WORK/out.txt" <<'PY' +import sys,re,json +S={} +for line in open(sys.argv[1]): + m=re.match(r'(STATS\d) (\{.*\})',line.strip()) + if m: S[m.group(1)]=json.loads(m.group(2)) +rc=0 +def check(c,msg): + global rc; print((" PASS: " if c else " FAIL: ")+msg) + if not c: rc=1 +s0,s1,s2=S["STATS0"],S["STATS1"],S["STATS2"] +# after creation, before any query +check(s0["aff_node_creates"]==5, f"node_creates==5 (got {s0['aff_node_creates']})") +check(s0["aff_ise_ingests"]==2, f"ise_ingests==2 (got {s0['aff_ise_ingests']})") +check(s0["aff_edge_creates"]==2, f"edge_creates==2 (got {s0['aff_edge_creates']})") +check(s0["aff_queries"]==0 and s0["aff_activations"]==0, "queries/activations start at 0") +# after 4 queries +check(s1["aff_queries"]==4, f"queries==4 (got {s1['aff_queries']})") +check(s1["aff_activations"]==4, f"activations==4 (got {s1['aff_activations']})") +check(s1["aff_node_creates"]==5 and s1["aff_ise_ingests"]==2 and s1["aff_edge_creates"]==2, + "create counters unchanged by queries") +# after 3 more queries — monotonic +check(s2["aff_queries"]==7, f"queries==7 monotonic (got {s2['aff_queries']})") +check(s2["aff_activations"]==7, f"activations==7 monotonic (got {s2['aff_activations']})") +check(s2["aff_queries"]>s1["aff_queries"]>s0["aff_queries"], "queries strictly monotonic across readings") +sys.exit(rc) +PY +[ $? -ne 0 ] && fail=1 + +echo +echo "== ASan+UBSan ==" +gcc -O1 -g -std=c11 -fsanitize=address,undefined -fno-sanitize-recover=undefined \ + -I "$INC" "$HERE/test_interoception_p4_afferent.c" "$RT" "$ST" "$GEO" "$VIDX" \ + -lcurl -lm -o "$WORK/p4.san" 2>"$WORK/san_cc.log" || { echo "SAN COMPILE FAILED"; tail -25 "$WORK/san_cc.log"; fail=1; } +if [ -x "$WORK/p4.san" ]; then + export ASAN_OPTIONS=detect_leaks=0 + "$WORK/p4.san" >/dev/null 2>"$WORK/san.log" + if grep -qiE 'runtime error|AddressSanitizer|Sanitizer|ERROR: ' "$WORK/san.log"; then + echo " FAIL: sanitizer findings:"; grep -iE 'runtime error|Sanitizer|ERROR' "$WORK/san.log" | head; fail=1 + else echo " ok: ASan+UBSan clean"; fi +fi + +echo +if [ "$fail" -eq 0 ]; then echo "====== P4 AFFERENT-COUNTERS GATE: PASS ======"; else echo "====== P4 AFFERENT-COUNTERS GATE: FAIL ======"; fi +rm -rf "$WORK" +exit $fail diff --git a/engram/test/test_interoception_p4_afferent.c b/engram/test/test_interoception_p4_afferent.c new file mode 100644 index 0000000..fcb99de --- /dev/null +++ b/engram/test/test_interoception_p4_afferent.c @@ -0,0 +1,35 @@ +/* test_interoception_p4_afferent.c — M-INTEROCEPTION Priority 4. + * Afferent input counters in engram_act_stats_json: additive observability. + * Drives KNOWN counts and asserts the emitted counters match and are monotonic. + */ +#include "el_runtime.h" +#include +#include +#include + +static el_val_t S(const char* s){ return EL_STR(s); } +static el_val_t F(double d){ return el_from_float(d); } + +int main(void){ + /* 3 plain node creates + 2 ISE creates = 5 node_creates, 2 ise_ingests */ + el_val_t a=engram_node_full(S("alpha concept about memory and time"),S("Concept"),S("a"),F(0.9),F(0.8),F(1.0),S("Semantic"),S("x")); + el_val_t b=engram_node_full(S("beta concept about memory and links"),S("Concept"),S("b"),F(0.9),F(0.8),F(1.0),S("Semantic"),S("x")); + engram_node_full(S("gamma distractor"),S("Fact"),S("c"),F(0.4),F(0.4),F(1.0),S("Semantic"),S("y")); + engram_node_full(S("heartbeat internal state one"),S("InternalStateEvent"),S("i1"),F(0.5),F(0.5),F(1.0),S("Working"),S("ise")); + engram_node_full(S("curiosity internal state two"),S("InternalStateEvent"),S("i2"),F(0.5),F(0.5),F(1.0),S("Working"),S("ise")); + /* 2 edge creates */ + engram_connect(a,b,F(0.8),S("associate")); + engram_connect(b,a,F(0.3),S("associate")); + + /* first reading (0 queries so far) */ + printf("STATS0 %s\n", EL_CSTR(engram_act_stats_json())); + + /* 4 queries -> 4 activations */ + for(int i=0;i<4;i++) engram_activate_json(S("memory and time and links"), (el_val_t)2); + printf("STATS1 %s\n", EL_CSTR(engram_act_stats_json())); + + /* 3 more queries -> monotonic increase */ + for(int i=0;i<3;i++) engram_activate_json(S("memory and time and links"), (el_val_t)2); + printf("STATS2 %s\n", EL_CSTR(engram_act_stats_json())); + return 0; +} diff --git a/lang/runtime/el_runtime.c b/lang/runtime/el_runtime.c index 4a47add..0265064 100644 --- a/lang/runtime/el_runtime.c +++ b/lang/runtime/el_runtime.c @@ -6534,6 +6534,15 @@ static int64_t _eg_embed_breaker_until = 0; * rates keep the previous reading and diff. Restart legitimately resets to 0. */ static int64_t _eg_act_breakthroughs = 0; /* forced promotions at the floor, cumulative */ static int64_t _eg_act_wm_evicted = 0; /* ALL WM evictions, cumulative (see below) */ +/* M-INTEROCEPTION P4: AFFERENT input counters — monotonic raw counts of the + * incoming signals the mind receives. Additive observability, MEASURED and + * rotated via the existing act-stats mechanism, NEVER accreted as memory nodes. + * Process-lifetime totals (reset to 0 on restart, like the other _eg_act_*). */ +static int64_t _eg_aff_activations = 0; /* engram_activate calls (core spreading) */ +static int64_t _eg_aff_queries = 0; /* engram_activate_json entries */ +static int64_t _eg_aff_node_creates = 0; /* engram_node_full calls */ +static int64_t _eg_aff_ise_ingests = 0; /* InternalStateEvent nodes created */ +static int64_t _eg_aff_edge_creates = 0; /* engram_connect calls */ /* Redundancy suppression counters (2026-08-05 self-review) — see * ENGRAM_DEDUP_COS. dup_seeds = semantic seed slots reclaimed from redundant * copies; dup_wm = WM candidates dropped for duplicating a higher-ranked @@ -7986,6 +7995,9 @@ el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label, engram_idmap_put(g, n->id, new_idx_full); engram_adj_on_node_added(g); if (engram_store_enabled()) eg_store_put_node(n); + /* P4 afferent counters: a node was created; ISE ingests counted separately. */ + _eg_aff_node_creates++; + if (n->node_type && strcmp(n->node_type, "InternalStateEvent") == 0) _eg_aff_ise_ingests++; /* P1 CONNECTION threshold: self-gated (inert unless ENGRAM_CONSOLIDATION). */ eg_consolidate_ise_connect(n); return el_wrap_str(el_strdup(n->id)); @@ -8549,6 +8561,7 @@ void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t const char* t = EL_CSTR(to_id); const char* r = EL_CSTR(relation); if (!f || !t) return; + _eg_aff_edge_creates++; /* P4 afferent counter: an edge was authored */ engram_grow_edges(); EngramEdge* e = &g->edges[g->edge_count]; memset(e, 0, sizeof(*e)); @@ -9222,6 +9235,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) { int64_t max_depth = (int64_t)depth; if (max_depth <= 0) max_depth = 2; el_val_t out = el_list_empty(); if (!q || g->node_count == 0) return out; + _eg_aff_activations++; /* P4 afferent counter: a real spreading activation ran */ /* Rebuild adjacency index if the edge/node topology changed since the * last activation call. This is O(E) one-time cost vs O(E) per BFS step @@ -12249,6 +12263,7 @@ el_val_t engram_activate_json(el_val_t query, el_val_t depth) { * Each entry includes both activation_strength (layer 1 background) and * working_memory_weight (layer 2 executive filter), plus promoted flag. * Callers performing context compilation should filter to promoted=1. */ + _eg_aff_queries++; /* P4 afferent counter: a query entered the mind */ el_val_t lst = engram_activate(query, depth); ElList* arr = (ElList*)(uintptr_t)lst; JsonBuf b; jb_init(&b); @@ -12455,7 +12470,7 @@ el_val_t engram_act_stats_json(void) { /* 768, not 512: the write-back gauges added 2026-08-07 push the worst-case * rendering past the old bound, and snprintf would truncate the JSON into * an unparseable tail rather than fail loudly. */ - char buf[896]; + char buf[1088]; /* ctx_cos (2026-07-29): cos(query, context centroid) at the LAST * activate call, measured before the query was folded in. ~1.0 = * context aligned with current query; low = divergence (expected at @@ -12476,7 +12491,12 @@ el_val_t engram_act_stats_json(void) { * any climb means a write path is mangling text again. Cheap * (counted at creation) — the full census lives in * engram_text_health_json. (2026-08-08 self-review) */ - "\"txt_damaged\":%lld}", + "\"txt_damaged\":%lld," + /* P4 afferent input counters (M-INTEROCEPTION): monotonic raw + * counts of incoming signals; MEASURED here, never memory nodes. */ + "\"aff_activations\":%lld,\"aff_queries\":%lld," + "\"aff_node_creates\":%lld,\"aff_ise_ingests\":%lld," + "\"aff_edge_creates\":%lld}", (long long)_eg_act_wm_evicted, (long long)_eg_act_breakthroughs, breaker_open, _eg_embed_consec_fail, @@ -12488,7 +12508,10 @@ el_val_t engram_act_stats_json(void) { (long long)_eg_hebb_wb_dropped, (long long)_eg_act_dup_seeds, (long long)_eg_act_dup_wm, (long long)_eg_act_dup_wm_global, - (long long)_eg_txt_write_damaged); + (long long)_eg_txt_write_damaged, + (long long)_eg_aff_activations, (long long)_eg_aff_queries, + (long long)_eg_aff_node_creates, (long long)_eg_aff_ise_ingests, + (long long)_eg_aff_edge_creates); return el_wrap_str(el_strdup(buf)); }