diff --git a/engram/test/run_interoception_p5.sh b/engram/test/run_interoception_p5.sh new file mode 100755 index 0000000..90e6c81 --- /dev/null +++ b/engram/test/run_interoception_p5.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# M-INTEROCEPTION P5 gate: dream-recall builtin engram_dreams_json (honesty rail). +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-p5-XXXXXX)" +export HOME="$WORK/home"; mkdir -p "$HOME" +unset ENGRAM_STORE +fail=0 + +echo "== compile ==" +gcc -O1 -std=c11 -I "$INC" "$HERE/test_interoception_p5_dreams.c" "$RT" "$ST" "$GEO" "$VIDX" \ + -lcurl -lm -o "$WORK/p5" 2>"$WORK/cc.log" || { echo "COMPILE FAILED"; cat "$WORK/cc.log"; rm -rf "$WORK"; exit 1; } + +D="$WORK/d"; mkdir -p "$D" +"$WORK/p5" "$D" > "$WORK/out.txt" 2>&1 || { echo "FAIL run"; cat "$WORK/out.txt"; fail=1; } +cat "$WORK/out.txt" | sed 's/^/ /' + +echo +echo "== assertions ==" +python3 - "$WORK/out.txt" <<'PY' +import sys,re,json +L={} +for line in open(sys.argv[1]): + line=line.strip() + m=re.match(r'(BEFORE|AFTER) (\[.*\])',line) + if m: L[m.group(1)]=json.loads(m.group(2)); continue + m=re.match(r'PRUNED (\d+)',line) + if m: L['PRUNED']=int(m.group(1)); continue + m=re.match(r'SINCE (\d+) (\[.*\])',line) + if m: L['SINCE']=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 +before_ids={d["id"] for d in L["BEFORE"]} +after_ids={d["id"] for d in L["AFTER"]} +since_ids={d["id"] for d in L["SINCE"]} +check(before_ids=={"cur_old","cur_mid","cur_recent"}, f"before prune: all 3 curiosity_scan, heartbeat excluded (got {sorted(before_ids)})") +check("hb_recent" not in before_ids, "heartbeat ISE never appears (not a dream)") +check(L["PRUNED"]==1, f"prune rotated out exactly the ancient ISE (pruned={L['PRUNED']})") +check(after_ids=={"cur_mid","cur_recent"}, f"after prune: rotated-out cur_old is ABSENT, not confabulated (got {sorted(after_ids)})") +check("cur_old" not in after_ids, "honesty rail: pruned dream is gone = 'I don't remember', never synthesized") +check(since_ids=={"cur_recent"}, f"since filter returns only events after the cutoff (got {sorted(since_ids)})") +# no fabrication: every returned id was one we seeded +seeded={"cur_old","cur_mid","cur_recent","hb_recent"} +allret=before_ids|after_ids|since_ids +check(allret<=seeded, f"no fabricated entries — every returned id was seeded ({sorted(allret)})") +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_p5_dreams.c" "$RT" "$ST" "$GEO" "$VIDX" \ + -lcurl -lm -o "$WORK/p5.san" 2>"$WORK/san_cc.log" || { echo "SAN COMPILE FAILED"; tail -25 "$WORK/san_cc.log"; fail=1; } +if [ -x "$WORK/p5.san" ]; then + export ASAN_OPTIONS=detect_leaks=0 + DS="$WORK/ds"; mkdir -p "$DS" + "$WORK/p5.san" "$DS" >/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 "====== P5 DREAM-RECALL GATE: PASS ======"; else echo "====== P5 DREAM-RECALL GATE: FAIL ======"; fi +rm -rf "$WORK" +exit $fail diff --git a/engram/test/test_interoception_p5_dreams.c b/engram/test/test_interoception_p5_dreams.c new file mode 100644 index 0000000..fe866fd --- /dev/null +++ b/engram/test/test_interoception_p5_dreams.c @@ -0,0 +1,47 @@ +/* test_interoception_p5_dreams.c — M-INTEROCEPTION Priority 5. + * Dream-recall-on-wake: engram_dreams_json(since_ms). Honesty rail — only + * curiosity_scan ISEs still resident are returned; pruned (rotated-out) ones are + * ABSENT (never confabulated); heartbeat ISEs are excluded. + */ +#include "el_runtime.h" +#include +#include +#include +#include + +static el_val_t S(const char* s){ return EL_STR(s); } + +int main(int argc,char** argv){ + if(argc<2){ fprintf(stderr,"usage: %s \n",argv[0]); return 2; } + const char* dir=argv[1]; + struct timeval tv; gettimeofday(&tv,NULL); + long long now=(long long)tv.tv_sec*1000+tv.tv_usec/1000; + long long mid=now-3600000; /* 1h ago */ + long long ancient=1000; /* pruned by 48h retention */ + + char p[1024]; snprintf(p,sizeof p,"%s/seed.json",dir); + FILE* f=fopen(p,"w"); + fprintf(f,"{\"nodes\":[" + "{\"id\":\"cur_old\",\"content\":\"{\\\"kind\\\":\\\"curiosity_scan\\\",\\\"q\\\":\\\"old wondering\\\"}\"," + "\"node_type\":\"InternalStateEvent\",\"label\":\"state-event\",\"created_at\":%lld}," + "{\"id\":\"cur_mid\",\"content\":\"{\\\"kind\\\":\\\"curiosity_scan\\\",\\\"q\\\":\\\"mid wondering\\\"}\"," + "\"node_type\":\"InternalStateEvent\",\"label\":\"state-event\",\"created_at\":%lld}," + "{\"id\":\"cur_recent\",\"content\":\"{\\\"kind\\\":\\\"curiosity_scan\\\",\\\"q\\\":\\\"recent wondering\\\"}\"," + "\"node_type\":\"InternalStateEvent\",\"label\":\"state-event\",\"created_at\":%lld}," + "{\"id\":\"hb_recent\",\"content\":\"{\\\"kind\\\":\\\"heartbeat\\\",\\\"wm\\\":3}\"," + "\"node_type\":\"InternalStateEvent\",\"label\":\"state-event\",\"created_at\":%lld}" + "],\"edges\":[]}", ancient, mid, now, now); + fclose(f); + if(!engram_load(S(p))){ fprintf(stderr,"load failed\n"); return 2; } + + /* before prune: all resident curiosity_scan after since=0 */ + printf("BEFORE %s\n", EL_CSTR(engram_dreams_json((el_val_t)0))); + /* prune 48h — cur_old (ancient) rotates out */ + long long pruned=(long long)(int64_t)engram_prune_telemetry((el_val_t)0); + printf("PRUNED %lld\n", pruned); + printf("AFTER %s\n", EL_CSTR(engram_dreams_json((el_val_t)0))); + /* since filter: only events created after 30 min ago -> cur_recent only */ + long long since=now-1800000; + printf("SINCE %lld %s\n", since, EL_CSTR(engram_dreams_json((el_val_t)(int64_t)since))); + return 0; +} diff --git a/lang/runtime/el_runtime.c b/lang/runtime/el_runtime.c index 0265064..535d3a5 100644 --- a/lang/runtime/el_runtime.c +++ b/lang/runtime/el_runtime.c @@ -12184,6 +12184,38 @@ el_val_t engram_scan_nodes_emb_json(el_val_t limit, el_val_t offset) { return el_wrap_str(b.buf); } +/* engram_dreams_json — M-INTEROCEPTION P5: dream-recall-on-wake (read-only). + * Returns the curiosity_scan InternalStateEvents created after `since_ms` that + * are STILL RESIDENT — "what I was chewing on while you were gone." HARD honesty + * rail: it reports only ISEs still in the buffer. Anything rotated out by the + * 48h engram_prune_telemetry is simply absent — rotated-out = "I don't + * remember", NEVER a synthesized/plausible dream. curiosity_scan is identified + * by the marker in the ISE's JSON content (the soul tags each ISE with its + * kind); heartbeat and other ISEs are excluded. Purely additive; no flag. */ +el_val_t engram_dreams_json(el_val_t since_ms) { + EngramStore* g = engram_get(); + int64_t since = (int64_t)since_ms; if (since < 0) since = 0; + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + int first = 1; + for (int64_t i = 0; i < g->node_count; i++) { + EngramNode* n = &g->nodes[i]; + if (!n->node_type || strcmp(n->node_type, "InternalStateEvent") != 0) continue; + if (n->created_at <= since) continue; + if (!n->content || !strstr(n->content, "curiosity_scan")) continue; + if (!first) jb_putc(&b, ','); + first = 0; + jb_putc(&b, '{'); + jb_puts(&b, "\"id\":"); jb_emit_escaped(&b, n->id ? n->id : ""); + char t[32]; snprintf(t, sizeof t, ",\"created_at\":%lld", (long long)n->created_at); + jb_puts(&b, t); + jb_puts(&b, ",\"content\":"); jb_emit_escaped(&b, n->content ? n->content : ""); + jb_putc(&b, '}'); + } + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction) { /* Re-implement here directly so we serialize without going through * the ElList path. Walks BFS to max_depth, emits {node, edge, hops} diff --git a/lang/runtime/el_runtime.h b/lang/runtime/el_runtime.h index 03702e7..73ad589 100644 --- a/lang/runtime/el_runtime.h +++ b/lang/runtime/el_runtime.h @@ -622,6 +622,7 @@ el_val_t engram_search_json(el_val_t query, el_val_t limit); el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset); el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset); el_val_t engram_scan_nodes_emb_json(el_val_t limit, el_val_t offset); +el_val_t engram_dreams_json(el_val_t since_ms); el_val_t engram_consolidate_permanence(el_val_t node_id); el_val_t engram_age_field(el_val_t delta_ms); el_val_t engram_age_field_catchup(void); diff --git a/lang/runtime/el_seed.c b/lang/runtime/el_seed.c index a5260a4..9dfa88b 100644 --- a/lang/runtime/el_seed.c +++ b/lang/runtime/el_seed.c @@ -1090,6 +1090,10 @@ el_val_t __engram_scan_nodes_emb_json(el_val_t limit, el_val_t offset) { return engram_scan_nodes_emb_json(limit, offset); } +el_val_t __engram_dreams_json(el_val_t since_ms) { + return engram_dreams_json(since_ms); +} + el_val_t __engram_consolidate_permanence(el_val_t node_id) { return engram_consolidate_permanence(node_id); }