Compare commits

..

8 Commits

Author SHA1 Message Date
will.anderson 99b113ea9d Merge branch 'stage' into feat/windows-el-runtime
El SDK CI - stage / build-and-test (pull_request) Failing after 15s
Resolve el_runtime.c conflict: include both sys/resource.h (from stage)
and el_closesocket POSIX shim (from Windows port) within the #else block.
2026-06-19 19:05:37 -05:00
will.anderson c087b97093 fix(windows): resolve PR blockers — nanosleep shim, unsetenv, duplicate typedefs, SOCKET type, el_closesocket
El SDK CI - stage / build-and-test (pull_request) Failing after 22s
2026-06-19 18:59:10 -05:00
tim.lingo 718a2e0c06 Merge pull request 'feat(engram): accumulation layer — new nodes to top of stack, not core-identity' (#59) from feat/accumulation-layer into stage
El SDK CI - stage / build-and-test (push) Failing after 8m50s
2026-06-17 18:34:05 +00:00
tim.lingo b6187501fd Merge pull request 'Reconcile live runtime data-integrity fixes onto main (UAF + atomic engram_save)' (#58) from fix/runtime-integrity-reconcile into stage
El SDK CI - stage / build-and-test (push) Failing after 9m32s
2026-06-17 18:33:16 +00:00
Tim Lingo 18e1ab6db1 feat(engram): add accumulation layer (layer 5) — new nodes default to it, not core-identity
El SDK Release / build-and-release (pull_request) Failing after 12m23s
Implements the accumulation layer from the Layered Consciousness architecture
(provisional 64/064,262) and answers the deferred design question. Per the spec
and Will's design: new user-facing nodes (memories, knowledge, conversations) are
created in an accumulation layer at the TOP of the consciousness stack — the engram
the user sees — while the layers below (safety, core-identity, domain, imprint,
suit) shape behavior but are hidden from the user.

- Adds ENGRAM_LAYER_ACCUMULATION (5) + the layer record in engram_init_layers
  (activation_priority 50, suppressible, not injectable, transparent=0).
- engram_node and engram_node_full now assign new nodes to ENGRAM_LAYER_ACCUMULATION.
- ENGRAM_LAYER_DEFAULT stays CORE_IDENTITY ON PURPOSE: it is the fallback for LEGACY
  nodes loaded from snapshots without a layer_id, so existing data (the originator
  corpus) is NEVER migrated. New-nodes-only — the immutable-originator rule.

This is the foundation for fixing the identity-bleed / customer-isolation issue
(user data was landing in Neuron's core-identity layer). The retrieval-side
provenance filter (introspection should compile from accumulation, not the
originator corpus — Persona 64/036,574) is a follow-on, pending the batch-2
Layered Consciousness + Engram spec docs for exact semantics. Compiles clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:14:57 -05:00
Tim Lingo 2dec76c87a fix(runtime): reconcile live data-integrity fixes onto main (UAF + atomic engram_save)
El SDK Release / build-and-release (pull_request) Failing after 17s
Ports the fixes that until now lived only in the un-versioned el-sdk source the live
macOS soul was hand-built from (captured in the [DO NOT MERGE] live-darwin-runtime
snapshot) FORWARD onto main, faithfully and minimally — without dragging in the
snapshot's deletions of main's newer engram_wm_/engram_load_merge/http_serve_async.

1. UAF (hallucinated/lost-saves root cause): engram_new_id + engram_node_full now use
   el_strdup_persist, NOT el_strdup. el_strdup tracks into the per-request arena that
   el_request_end() frees when the creating HTTP request completes — leaving stored
   nodes with dangling pointers (corrupted ids, 'saved but never listed'). Transplanted
   verbatim from the live runtime; el_strdup_persist sites 19->27, matching live.

2. Atomic engram_save: write <path>.tmp, fflush+fsync, rename() over target (atomic on
   POSIX) so a booting soul's engram_load never reads a truncated/0-byte snapshot — the
   genesis -> nodes=1 -> 63-node-clobber loop. Plus a sparse-write floor: refuse to
   overwrite a >200KB snapshot with one < 1/16 its size. (Validated in isolation:
   harness 11/11; rebuilt+booted the darwin soul, round-tripped 5113 nodes, no clobber.)

The response-truncation fix is already on main (_tl_fs_read_len binary-safe length).
Compiles clean. For Will to build through CI/elb and deploy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 19:46:56 -05:00
Tim Lingo a36a62ca14 fix(el-runtime): promote http_handler typedefs to el_runtime.h (cross-module + Windows)
El SDK Release / build-and-release (pull_request) Failing after 13m0s
http_handler_fn / http_handler4_fn were defined only inside el_runtime.c, so soul
modules (routes/chat/...) that reference them via cross-module forward declarations
couldn't see the types — which broke the Windows link of every module. Moving the
public function-pointer types to the shared header is the correct home and unblocks
the build on all platforms (identical typedef, C11-safe redefinition in el_runtime.c).

With this, the soul links into a native Windows neuron.exe (mingw, static) that boots
and serves HTTP on :7770 — verified /health → 200 {"status":"alive",...} in a Win11 VM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 17:14:17 -05:00
Tim Lingo 28ef43264a feat(el-runtime): native Windows port of el_runtime.c (winsock/dlsym/CreateProcess)
Compiles for Windows x64 via mingw-w64 and still compiles clean on POSIX
(darwin/linux) — all Windows code is behind #ifdef _WIN32, POSIX path unchanged.

- el_platform_win.h (new): winsock2 + auto WSAStartup, el_closesocket(),
  dlsym->GetProcAddress, popen/_popen, mkdir/_mkdir, setenv/_putenv_s,
  timegm/_mkgmtime, localtime_r/gmtime_r. Threading unchanged — mingw
  winpthreads supplies <pthread.h> + -lpthread.
- el_runtime.c: include block guarded; 10 socket-close sites -> el_closesocket();
  setsockopt arg4 cast; tm_zone guarded; exec_bg fork/exec -> CreateProcess.

Part of feat/windows-port. Core-el change, for Will's review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:58:11 -05:00
8 changed files with 254 additions and 524 deletions
BIN
View File
Binary file not shown.
+2 -38
View File
@@ -20,8 +20,6 @@ el_val_t route_create_edge(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_neighbors(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_strengthen(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_create_ise(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_sync(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);
@@ -117,7 +115,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 (salience == el_from_float(0.0)) {
if (str_eq(salience, el_from_float(0.0))) {
salience = el_from_float(0.5);
}
el_val_t id = engram_node(content, node_type, salience);
@@ -207,7 +205,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 (weight == el_from_float(0.0)) {
if (str_eq(weight, el_from_float(0.0))) {
weight = el_from_float(0.5);
}
engram_connect(from_id, to_id, weight, relation);
@@ -245,34 +243,6 @@ el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body) {
return 0;
}
el_val_t route_create_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 route_sync(el_val_t method, el_val_t path, el_val_t body) {
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
if (str_eq(dir, EL_STR(""))) {
dir = EL_STR("/tmp/engram");
}
el_val_t snap_path = el_str_concat(dir, EL_STR("/sync-export.json"));
engram_save(snap_path);
el_val_t snap = fs_read(snap_path);
if (str_eq(snap, EL_STR(""))) {
return EL_STR("{\"nodes\":[],\"edges\":[]}");
}
return snap;
return 0;
}
el_val_t route_save(el_val_t method, el_val_t path, el_val_t body) {
el_val_t p = json_get_string(body, EL_STR("path"));
if (str_eq(p, EL_STR(""))) {
@@ -329,9 +299,6 @@ 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_starts_with(clean, EL_STR("/api/neuron/state-events"))) {
return route_create_ise(method, path, body);
}
if (!check_auth_ok(method, body)) {
return err_json(EL_STR("unauthorized"));
}
@@ -374,9 +341,6 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/strengthen")) || str_eq(clean, EL_STR("/strengthen")))) {
return route_strengthen(method, path, body);
}
if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/sync")) || str_eq(clean, EL_STR("/sync")))) {
return route_sync(method, path, body);
}
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/save")) || str_eq(clean, EL_STR("/save")))) {
return route_save(method, path, body);
}
-50
View File
@@ -180,43 +180,6 @@ fn route_forget(method: String, path: String, body: String) -> String {
ok_json()
}
// route_create_ise POST /api/neuron/state-events
// Creates an InternalStateEvent node from a JSON body with a "content" field.
// Returns {"ok":true,"id":"<uuid>"}. Used by the soul daemon's ise_post() to
// record internal state transitions in the authoritative Engram store.
// This route was in the original server.el but was lost during a refactor;
// its absence would break ISE recording on the next Engram restart.
// (Restored 2026-06-30 self-review)
// importance=0.3 matches awareness.el in-process fallback (engram_node_full ISE
// defaults). Original had 0.5 which was a mismatch. (Corrected 2026-06-30)
fn route_create_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 + "\"}"
}
// route_sync GET /api/sync
// Returns the full graph snapshot as JSON (nodes + edges), suitable for loading
// via engram_load_merge. Used by the soul daemon's periodic refresh cycle to
// keep its in-process Engram store in sync with this authoritative HTTP store.
// Saves a temporary snapshot to avoid holding a large in-memory string while
// streaming caller reads the file through the HTTP response body.
// (Restored 2026-06-30 self-review)
fn route_sync(method: String, path: String, body: String) -> String {
let dir: String = env("ENGRAM_DATA_DIR")
if str_eq(dir, "") { let dir = "/tmp/engram" }
let snap_path: String = dir + "/sync-export.json"
engram_save(snap_path)
let snap: String = fs_read(snap_path)
if str_eq(snap, "") { return "{\"nodes\":[],\"edges\":[]}" }
snap
}
fn route_save(method: String, path: String, body: String) -> String {
let p: String = json_get_string(body, "path")
if str_eq(p, "") {
@@ -269,14 +232,6 @@ fn handle_request(method: String, path: String, body: String) -> String {
}
}
// Internal state events bypass auth only the local soul daemon calls
// this route, and ise_post() does not include an _auth field in its body.
// Placing this before the auth gate preserves the old binary's behavior
// and ensures the soul daemon can always write ISEs.
if str_eq(method, "POST") && str_starts_with(clean, "/api/neuron/state-events") {
return route_create_ise(method, path, body)
}
// Auth (when ENGRAM_API_KEY is set)
if !check_auth_ok(method, body) {
return err_json("unauthorized")
@@ -331,11 +286,6 @@ fn handle_request(method: String, path: String, body: String) -> String {
return route_strengthen(method, path, body)
}
// Sync soul daemon fetches here for periodic in-process graph refresh
if str_eq(method, "GET") && (str_eq(clean, "/api/sync") || str_eq(clean, "/sync")) {
return route_sync(method, path, body)
}
// Persistence
if str_eq(method, "POST") && (str_eq(clean, "/api/save") || str_eq(clean, "/save")) {
return route_save(method, path, body)
+117
View File
@@ -0,0 +1,117 @@
#ifndef EL_PLATFORM_WIN_H
#define EL_PLATFORM_WIN_H
/*
* el_platform_win.h — Windows OS-boundary shim for el_runtime.c.
*
* Branch: feat/windows-el-runtime. Included ONLY when _WIN32 is defined; the POSIX build is
* untouched. Goal: let el_runtime.c (a BSD-sockets / dlfcn / fork host) compile and link with
* mingw-w64 into a native neuron.exe, with no behavioural change to the Linux/macOS build.
*
* What it maps:
* - sockets : winsock2 (same call names: socket/bind/listen/accept/recv/send/setsockopt).
* Sockets close with closesocket() (see el_closesocket), and the stack must be
* started once with WSAStartup — done automatically via a load-time constructor.
* - dlsym : el_runtime.c uses dlsym(RTLD_DEFAULT, name) to resolve callback/tool symbols
* exported by the main module. Windows equivalent: GetProcAddress on the process
* module. Link the soul with -Wl,--export-all-symbols so the symbols are findable.
* - popen : mapped to _popen/_pclose.
* - threads : UNCHANGED. mingw-w64 ships winpthreads, so <pthread.h> + -lpthread just work.
*/
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <io.h>
#include <process.h>
/* Portable headers mingw-w64 provides (verified present). */
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h> /* strcasecmp */
#include <ctype.h>
#include <math.h>
#include <time.h>
#include <sys/time.h> /* mingw-w64 provides gettimeofday here */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <errno.h>
#include <pthread.h>
/* ── socket close ─────────────────────────────────────────────────────────── */
/* Winsock closes sockets with closesocket(), not close() (close() is for file fds). The POSIX
build defines the same helper as close() so the call sites are identical across platforms. */
static inline int el_closesocket(SOCKET s) { return closesocket(s); }
/* ── winsock init (once, at load) ─────────────────────────────────────────── */
static void el__win_net_init(void) {
static int inited = 0;
if (!inited) { WSADATA w; WSAStartup(MAKEWORD(2, 2), &w); inited = 1; }
}
__attribute__((constructor)) static void el__win_ctor(void) { el__win_net_init(); }
/* ── dlsym → GetProcAddress ───────────────────────────────────────────────── */
#ifndef RTLD_DEFAULT
#define RTLD_DEFAULT ((void*)0)
#endif
static inline void* el_win_dlsym(void* handle, const char* name) {
(void)handle;
return (void*)(uintptr_t)GetProcAddress(GetModuleHandleA(NULL), name);
}
#define dlsym(h, n) el_win_dlsym((h), (n))
/* ── popen / pclose ───────────────────────────────────────────────────────── */
#define popen _popen
#define pclose _pclose
/* ── misc POSIX → Win32 shims ─────────────────────────────────────────────── */
#include <direct.h> /* _mkdir */
#define mkdir(path, mode) _mkdir(path) /* POSIX mkdir(path,mode) → _mkdir(path) */
#define timegm _mkgmtime /* UTC tm → time_t */
/* setenv/unsetenv: not in the Windows CRT; map to _putenv_s / SetEnvironmentVariable. */
static inline int setenv(const char* name, const char* value, int overwrite) {
(void)overwrite;
return _putenv_s(name, value ? value : "");
}
static inline int unsetenv(const char* name) {
/* _putenv_s(name, "") sets VAR="" rather than removing it.
* SetEnvironmentVariableA(name, NULL) truly deletes it from the Win32
* env block; then we sync the CRT cache with _putenv("NAME="). */
SetEnvironmentVariableA(name, NULL);
size_t len = strlen(name);
char *buf = (char*)malloc(len + 2);
if (!buf) return -1;
memcpy(buf, name, len);
buf[len] = '=';
buf[len + 1] = '\0';
_putenv(buf);
free(buf);
return 0;
}
/* nanosleep — not available in MSVC/UCRT; approximate with Sleep(). */
static inline int el_nanosleep(const struct timespec *req, struct timespec *rem) {
(void)rem;
DWORD ms = (DWORD)((req->tv_sec * 1000ULL) + (req->tv_nsec / 1000000ULL));
Sleep(ms ? ms : 1);
return 0;
}
#define nanosleep(req, rem) el_nanosleep((req), (rem))
/* localtime_r/gmtime_r: Windows offers localtime_s/gmtime_s with reversed arg order. */
static inline struct tm* localtime_r(const time_t* t, struct tm* out) {
return localtime_s(out, t) == 0 ? out : (struct tm*)0;
}
static inline struct tm* gmtime_r(const time_t* t, struct tm* out) {
return gmtime_s(out, t) == 0 ? out : (struct tm*)0;
}
#endif /* EL_PLATFORM_WIN_H */
+126 -34
View File
@@ -21,6 +21,10 @@
#include "el_runtime.h"
#ifdef _WIN32
/* Windows OS-boundary shim (winsock/dlsym/popen). Threading stays on <pthread.h> (winpthreads). */
#include "el_platform_win.h"
#else
#include <stdarg.h>
#include <strings.h> /* strcasecmp */
#include <stdint.h>
@@ -43,6 +47,10 @@
#include <errno.h>
#include <pthread.h>
#include <sys/resource.h> /* getrusage — memory guard */
/* On POSIX, sockets close with the same close() as files; el_platform_win.h supplies the Windows
variant. Defined here so the socket call sites are identical across platforms. */
static inline int el_closesocket(int s) { return close(s); }
#endif
#ifdef HAVE_CURL
#include <curl/curl.h>
#endif
@@ -1054,7 +1062,6 @@ el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el
#define HTTP_MAX_CONNS 64
typedef el_val_t (*http_handler_fn)(el_val_t method, el_val_t path, el_val_t body);
typedef struct {
char* name;
@@ -1529,12 +1536,20 @@ static void http_send_response(int fd, const char* body) {
}
typedef struct {
#ifdef _WIN32
SOCKET fd;
#else
int fd;
#endif
} HttpWorkerArg;
static void* http_worker(void* arg) {
HttpWorkerArg* a = (HttpWorkerArg*)arg;
#ifdef _WIN32
SOCKET fd = a->fd;
#else
int fd = a->fd;
#endif
free(a);
char *method = NULL, *path = NULL, *body = NULL;
if (http_read_request(fd, &method, &path, &body, NULL) == 0) {
@@ -1566,7 +1581,7 @@ static void* http_worker(void* arg) {
free(response);
}
free(method); free(path); free(body);
close(fd);
el_closesocket(fd);
/* release a slot */
pthread_mutex_lock(&_http_conn_mu);
_http_conn_active--;
@@ -1588,22 +1603,26 @@ el_val_t http_serve(el_val_t port, el_val_t handler) {
int sock = socket(AF_INET6, SOCK_STREAM, 0);
if (sock < 0) { perror("socket"); return 0; }
int yes = 1; int no = 0;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no));
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&yes, sizeof(yes));
setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, (const char*)&no, sizeof(no));
struct sockaddr_in6 addr;
memset(&addr, 0, sizeof(addr));
addr.sin6_family = AF_INET6;
addr.sin6_addr = in6addr_any;
addr.sin6_port = htons((uint16_t)p);
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind"); close(sock); return 0;
perror("bind"); el_closesocket(sock); return 0;
}
if (listen(sock, 64) < 0) { perror("listen"); close(sock); return 0; }
if (listen(sock, 64) < 0) { perror("listen"); el_closesocket(sock); return 0; }
fprintf(stderr, "[http] listening on [::]:%d (dual-stack)\n", p);
while (1) {
struct sockaddr_in6 cli;
socklen_t clen = sizeof(cli);
#ifdef _WIN32
SOCKET cfd = accept(sock, (struct sockaddr*)&cli, &clen);
#else
int cfd = accept(sock, (struct sockaddr*)&cli, &clen);
#endif
if (cfd < 0) {
if (errno == EINTR) continue;
perror("accept"); break;
@@ -1615,11 +1634,11 @@ el_val_t http_serve(el_val_t port, el_val_t handler) {
_http_conn_active++;
pthread_mutex_unlock(&_http_conn_mu);
HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg));
if (!arg) { close(cfd); continue; }
if (!arg) { el_closesocket(cfd); continue; }
arg->fd = cfd;
pthread_t tid;
if (pthread_create(&tid, NULL, http_worker, arg) != 0) {
close(cfd); free(arg);
el_closesocket(cfd); free(arg);
pthread_mutex_lock(&_http_conn_mu);
_http_conn_active--;
pthread_cond_signal(&_http_conn_cv);
@@ -1628,7 +1647,7 @@ el_val_t http_serve(el_val_t port, el_val_t handler) {
}
pthread_detach(tid);
}
close(sock);
el_closesocket(sock);
return 0;
}
@@ -1649,8 +1668,6 @@ el_val_t http_serve(el_val_t port, el_val_t handler) {
* separate active-handler slot, separate dlsym fallback. Mixing v1 and v2
* handlers in the same process is fine they don't share the active slot. */
typedef el_val_t (*http_handler4_fn)(el_val_t method, el_val_t path,
el_val_t headers_map, el_val_t body);
typedef struct {
char* name;
@@ -1786,7 +1803,11 @@ static el_val_t http_build_headers_map(const char* hdr_block) {
static void* http_worker_v2(void* arg) {
HttpWorkerArg* a = (HttpWorkerArg*)arg;
#ifdef _WIN32
SOCKET fd = a->fd;
#else
int fd = a->fd;
#endif
free(a);
char *method = NULL, *path = NULL, *body = NULL, *hdr_block = NULL;
if (http_read_request(fd, &method, &path, &body, &hdr_block) == 0) {
@@ -1816,7 +1837,7 @@ static void* http_worker_v2(void* arg) {
free(response);
}
free(method); free(path); free(body); free(hdr_block);
close(fd);
el_closesocket(fd);
pthread_mutex_lock(&_http_conn_mu);
_http_conn_active--;
pthread_cond_signal(&_http_conn_cv);
@@ -1838,22 +1859,26 @@ el_val_t http_serve_v2(el_val_t port, el_val_t handler) {
int sock = socket(AF_INET6, SOCK_STREAM, 0);
if (sock < 0) { perror("socket"); return 0; }
int yes = 1; int no = 0;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no));
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&yes, sizeof(yes));
setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, (const char*)&no, sizeof(no));
struct sockaddr_in6 addr;
memset(&addr, 0, sizeof(addr));
addr.sin6_family = AF_INET6;
addr.sin6_addr = in6addr_any;
addr.sin6_port = htons((uint16_t)p);
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind"); close(sock); return 0;
perror("bind"); el_closesocket(sock); return 0;
}
if (listen(sock, 64) < 0) { perror("listen"); close(sock); return 0; }
if (listen(sock, 64) < 0) { perror("listen"); el_closesocket(sock); return 0; }
fprintf(stderr, "[http v2] listening on [::]:%d (dual-stack)\n", p);
while (1) {
struct sockaddr_in6 cli;
socklen_t clen = sizeof(cli);
#ifdef _WIN32
SOCKET cfd = accept(sock, (struct sockaddr*)&cli, &clen);
#else
int cfd = accept(sock, (struct sockaddr*)&cli, &clen);
#endif
if (cfd < 0) {
if (errno == EINTR) continue;
perror("accept"); break;
@@ -1865,11 +1890,11 @@ el_val_t http_serve_v2(el_val_t port, el_val_t handler) {
_http_conn_active++;
pthread_mutex_unlock(&_http_conn_mu);
HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg));
if (!arg) { close(cfd); continue; }
if (!arg) { el_closesocket(cfd); continue; }
arg->fd = cfd;
pthread_t tid;
if (pthread_create(&tid, NULL, http_worker_v2, arg) != 0) {
close(cfd); free(arg);
el_closesocket(cfd); free(arg);
pthread_mutex_lock(&_http_conn_mu);
_http_conn_active--;
pthread_cond_signal(&_http_conn_cv);
@@ -1878,7 +1903,7 @@ el_val_t http_serve_v2(el_val_t port, el_val_t handler) {
}
pthread_detach(tid);
}
close(sock);
el_closesocket(sock);
return 0;
}
@@ -2129,6 +2154,23 @@ el_val_t exec(el_val_t cmdv) {
el_val_t exec_bg(el_val_t cmdv) {
const char* cmd = EL_CSTR(cmdv);
if (!cmd || !*cmd) return el_wrap_str(el_strdup(""));
#ifdef _WIN32
/* Windows: no fork/exec. Launch a detached `cmd /c <command>` with no console window via
CreateProcess (DETACHED_PROCESS | CREATE_NO_WINDOW). Returns the PID as a string, "" on fail.
Mirrors the POSIX branch: child runs independently, caller is not blocked. */
char cmdline[8192];
snprintf(cmdline, sizeof(cmdline), "cmd.exe /c %s", cmd);
STARTUPINFOA si; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si);
PROCESS_INFORMATION pi; ZeroMemory(&pi, sizeof(pi));
BOOL ok = CreateProcessA(NULL, cmdline, NULL, NULL, FALSE,
DETACHED_PROCESS | CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
if (!ok) return el_wrap_str(el_strdup(""));
char pidbuf[32];
snprintf(pidbuf, sizeof(pidbuf), "%lu", (unsigned long)pi.dwProcessId);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return el_wrap_str(el_strdup(pidbuf));
#else
pid_t pid = fork();
if (pid < 0) {
/* fork failed */
@@ -2151,6 +2193,7 @@ el_val_t exec_bg(el_val_t cmdv) {
char pidbuf[32];
snprintf(pidbuf, sizeof(pidbuf), "%d", (int)pid);
return el_wrap_str(el_strdup(pidbuf));
#endif
}
el_val_t fs_list(el_val_t pathv) {
@@ -4478,7 +4521,12 @@ static int _el_decompose_earth(el_caltime_t* ct, struct tm* tm_out, int* abbr_le
localtime_r(&s, &tm);
*tm_out = tm;
if (abbr_buf && abbr_cap > 0) {
/* mingw's struct tm has no tm_zone (BSD/glibc extension); no abbrev available there. */
#ifdef _WIN32
const char* z_str = "";
#else
const char* z_str = tm.tm_zone ? tm.tm_zone : "";
#endif
size_t n = strlen(z_str);
if (n >= abbr_cap) n = abbr_cap - 1;
memcpy(abbr_buf, z_str, n);
@@ -6031,6 +6079,14 @@ void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
#define ENGRAM_LAYER_DOMAIN 2u
#define ENGRAM_LAYER_IMPRINT 3u
#define ENGRAM_LAYER_SUIT 4u
#define ENGRAM_LAYER_ACCUMULATION 5u
/* New user-facing nodes (memories, knowledge, conversations) are created in the
* accumulation layer the top of the consciousness stack, the engram the user
* sees; every layer below shapes behavior but is hidden from the user (Layered
* Consciousness architecture, app 64/064,262). ENGRAM_LAYER_DEFAULT stays
* core-identity ON PURPOSE: it is the fallback home for LEGACY nodes loaded from
* snapshots without a layer_id, so existing data (the originator corpus) is
* never migrated out of its established layer. New != legacy. */
#define ENGRAM_LAYER_DEFAULT ENGRAM_LAYER_CORE_IDENTITY
/* Pass 3 override floor. Layer 0 nodes that received any background
@@ -6208,6 +6264,20 @@ static void engram_init_layers(EngramStore* g) {
.transparent = 0,
.injectable = 1
};
/* Layer 5 — accumulation. The TOP of the consciousness stack: the default
* home for all new user-facing nodes. This is the engram the user sees;
* every layer below shapes behavior but is hidden from the user. Not
* injectable it is the persistent user accumulation, not a swappable
* overlay. transparent=0: its content is surfaced to introspection (it is
* the user's own knowledge/memory), unlike the lower behavioral layers. */
g->layers[g->layer_count++] = (EngramLayer){
.layer_id = ENGRAM_LAYER_ACCUMULATION,
.name = el_strdup_persist("accumulation"),
.activation_priority = 50,
.suppressible = 1,
.transparent = 0,
.injectable = 0
};
}
static EngramStore* engram_get(void) {
@@ -6322,7 +6392,9 @@ static void engram_grow_edges(void) {
static char* engram_new_id(void) {
el_val_t v = uuid_new();
const char* s = EL_CSTR(v);
return el_strdup(s ? s : "");
/* Persistent: node ids live in the global store; an arena (el_strdup) id is
* freed at el_request_end(), corrupting the node after the creating request. */
return el_strdup_persist(s ? s : "");
}
/* Convert a node into an ElMap of its fields. */
@@ -6399,7 +6471,7 @@ el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience) {
n->last_activated = now;
n->created_at = now;
n->updated_at = now;
n->layer_id = ENGRAM_LAYER_DEFAULT;
n->layer_id = ENGRAM_LAYER_ACCUMULATION; /* new user-facing node → top layer */
g->node_count++;
return el_wrap_str(el_strdup(n->id));
}
@@ -6417,12 +6489,17 @@ el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label,
const char* lb = EL_CSTR(label);
const char* ti = EL_CSTR(tier);
const char* tg = EL_CSTR(tags);
n->content = el_strdup(c ? c : "");
n->node_type = el_strdup(nt && *nt ? nt : "Memory");
n->label = el_strdup(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : ""));
n->tier = el_strdup(ti && *ti ? ti : "Working");
n->tags = el_strdup(tg ? tg : "");
n->metadata = el_strdup("{}");
/* Persistent (el_strdup_persist, NOT el_strdup): these strings are owned by the
* persistent global node store. el_strdup tracks into the per-request arena, which
* el_request_end() frees when the creating HTTP request completes leaving the
* stored node with dangling pointers (corrupted ids, "saved but never listed").
* This is the root cause of the hallucinated/lost-saves class of bugs. */
n->content = el_strdup_persist(c ? c : "");
n->node_type = el_strdup_persist(nt && *nt ? nt : "Memory");
n->label = el_strdup_persist(lb && *lb ? lb : (c ? engram_first_n_chars(c, 60) : ""));
n->tier = el_strdup_persist(ti && *ti ? ti : "Working");
n->tags = el_strdup_persist(tg ? tg : "");
n->metadata = el_strdup_persist("{}");
n->salience = engram_decode_score(salience);
n->importance = engram_decode_score(importance);
n->confidence = engram_decode_score(confidence);
@@ -6435,7 +6512,7 @@ el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label,
n->last_activated = now;
n->created_at = now;
n->updated_at = now;
n->layer_id = ENGRAM_LAYER_DEFAULT;
n->layer_id = ENGRAM_LAYER_ACCUMULATION; /* new user-facing node → top layer */
g->node_count++;
return el_wrap_str(el_strdup(n->id));
}
@@ -7365,13 +7442,28 @@ el_val_t engram_save(el_val_t path) {
jb_putc(&b, '}');
}
jb_puts(&b, "]}");
FILE* f = fopen(p, "wb");
if (!f) { free(b.buf); return 0; }
{
struct stat _st;
if (stat(p, &_st) == 0 && _st.st_size > 200000 &&
(uint64_t)b.len < (uint64_t)_st.st_size / 16) {
fprintf(stderr, "[engram_save] REFUSED sparse write: new %zu vs existing %lld (<1/16) protecting %s\n",
b.len, (long long)_st.st_size, p);
free(b.buf); return 0;
}
}
size_t _plen = strlen(p);
char* _tmp = (char*)malloc(_plen + 5);
if (!_tmp) { free(b.buf); return 0; }
memcpy(_tmp, p, _plen); memcpy(_tmp + _plen, ".tmp", 5);
FILE* f = fopen(_tmp, "wb");
if (!f) { free(_tmp); free(b.buf); return 0; }
size_t w = fwrite(b.buf, 1, b.len, f);
fclose(f);
int ok = (w == b.len);
free(b.buf);
return ok ? 1 : 0;
int wok = (w == b.len);
if (wok) { fflush(f); fsync(fileno(f)); }
fclose(f); free(b.buf);
if (!wok) { unlink(_tmp); free(_tmp); return 0; }
if (rename(_tmp, p) != 0) { unlink(_tmp); free(_tmp); return 0; }
free(_tmp); return 1;
}
/* Helper: extract a string field from a JSON object substring. */
+6
View File
@@ -52,6 +52,12 @@
typedef int64_t el_val_t;
/* HTTP request-handler function-pointer types. Public because soul modules (routes/chat/etc.)
* register handlers across translation units; previously defined only inside el_runtime.c, which
* made cross-module references (and the Windows build) fail. Home in the shared header. */
typedef el_val_t (*http_handler_fn)(el_val_t method, el_val_t path, el_val_t body);
typedef el_val_t (*http_handler4_fn)(el_val_t method, el_val_t path, el_val_t body, el_val_t headers);
#define EL_STR(s) ((el_val_t)(uintptr_t)(s))
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
#define EL_INT(v) (v)
+3 -395
View File
@@ -5470,35 +5470,9 @@ 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
/* ENGRAM_BREAKTHROUGH_WEIGHT: lowered 0.25→0.10 (2026-06-30 self-review, porting
* fix from self-review 2026-06-26 branch). With 0.25, Knowledge nodes (threshold
* 0.15) promoted at ~0.21 decay in one call to ~0.147, fall below the 0.25 floor,
* and immediately lose their WM slot to fresh breakthrough candidates at 0.25.
* Natural promotion was invisible: live data showed 524/525 WM nodes at 0.25
* breakthrough floor. With 0.10, all per-type thresholds (minimum 0.15 Canonical)
* exceed the floor, so naturally-promoted nodes survive multiple decay cycles.
* Invariant maintained: BREAKTHROUGH_WEIGHT < min(type_thresholds). */
#define ENGRAM_BREAKTHROUGH_WEIGHT 0.10
/* ENGRAM_WM_CAP: hard limit on concurrent working-memory nodes (2026-06-30
* self-review, porting fix from self-review 2026-06-26 branch). 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 heartbeat ISEs show useless WM composition data. 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 nodes). */
#define ENGRAM_WM_CAP 24
#define ENGRAM_BREAKTHROUGH_WEIGHT 0.25
#define ENGRAM_INHIBITION_FACTOR 0.1
/* 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;
}
/* ── Layered consciousness architecture ──────────────────────────────────────
*
* The engram graph is stratified into LAYERS that gate which suppressions
@@ -6685,19 +6659,6 @@ 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-30 self-review: porting fix from 2026-06-26 branch; SYNAPSE
* paper confirms WM should hold only semantically relevant content.) */
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. */
@@ -6749,123 +6710,9 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
n->suppression_count = 0;
}
/* ── 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. (Ported from 2026-06-26 self-review branch; observed
* 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 lowest 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 to handle ties correctly. */
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.
*
* Conversational thread continuity (ENGRAM_WM_DECAY):
* Nodes promoted in a previous turn but NOT reached by the current BFS
* fan-out retain a decayed weight rather than being zeroed. This models
* the brain's ability to maintain recent context across successive turns
* without requiring explicit re-activation. A node that was relevant one
* query ago stays weakly present in working memory; a node from two
* queries ago retains 0.7² 0.49 of its original weight; after ~5 quiet
* turns it falls below 0.01 and is effectively evicted (set to 0.0).
*
* NOTE: this was documented in the ENGRAM_WM_DECAY constant comment since
* the two-layer architecture was introduced, but was never implemented
* unreached nodes were always zeroed unconditionally. Fixed 2026-06-30
* self-review. */
/* Persist working_memory_weight (post Pass 3) to node store. */
for (int64_t i = 0; i < g->node_count; i++) {
if (!reached[i] && g->nodes[i].working_memory_weight > 0.0) {
/* Carry-over decay: node held WM weight from prior activation but
* the current query's BFS fan-out did not reach it. Apply decay
* rather than zero so recently-active context persists. */
double decayed = g->nodes[i].working_memory_weight * ENGRAM_WM_DECAY;
g->nodes[i].working_memory_weight = (decayed < 0.01) ? 0.0 : decayed;
} else {
g->nodes[i].working_memory_weight = wm_weights[i];
}
}
/* ── PASS 5: Global WM cap enforcement ───────────────────────────────────
* Pass 4 capped this call's new candidates. But nodes already in WM from
* prior calls retain their persisted working_memory_weight (via the decay
* carry-over above). 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. (Ported from 2026-06-26
* self-review branch.) */
{
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. */
}
g->nodes[i].working_memory_weight = wm_weights[i];
}
/* ── Collect all background-activated nodes for the return value ────
@@ -7234,156 +7081,6 @@ el_val_t engram_load(el_val_t path) {
return 1;
}
/* engram_load_merge — like engram_load but WITHOUT resetting the store.
* Reads a JSON snapshot from `path` and adds any nodes/edges not already
* present in the in-memory graph. Dedup is by node id (for nodes) and by
* (from_id, to_id, relation) tuple (for edges).
*
* Returns (as an EL int) the count of new nodes added. Used by the soul
* daemon's periodic refresh cycle to keep its in-process Engram in sync
* with the HTTP Engram store without losing current working memory state.
* Ported from el-compiler/runtime on 2026-06-30 self-review. */
el_val_t engram_load_merge(el_val_t path) {
const char* p = EL_CSTR(path);
if (!p || !*p) return 0;
FILE* f = fopen(p, "rb");
if (!f) return 0;
fseek(f, 0, SEEK_END);
long sz = ftell(f);
rewind(f);
if (sz <= 0) { fclose(f); return 0; }
char* data = malloc((size_t)sz + 1);
if (!data) { fclose(f); return 0; }
size_t got = fread(data, 1, (size_t)sz, f);
fclose(f);
data[got] = '\0';
EngramStore* g = engram_get();
int64_t added_nodes = 0;
/* Walk nodes array — skip any node whose id already exists */
const char* nodes_p = json_find_key(data, "nodes");
if (nodes_p) {
nodes_p = eg_skip_ws(nodes_p);
if (*nodes_p == '[') {
nodes_p++;
nodes_p = eg_skip_ws(nodes_p);
while (*nodes_p && *nodes_p != ']') {
if (*nodes_p != '{') { nodes_p++; continue; }
const char* end = json_skip_value(nodes_p);
size_t n = (size_t)(end - nodes_p);
char* obj = malloc(n + 1);
memcpy(obj, nodes_p, n); obj[n] = '\0';
char* nid = eg_get_str_field(obj, "id");
int already = (nid && *nid && engram_find_node(nid) != NULL);
free(nid);
if (!already) {
engram_grow_nodes();
EngramNode* nn = &g->nodes[g->node_count];
memset(nn, 0, sizeof(*nn));
nn->id = eg_get_str_field(obj, "id");
nn->content = eg_get_str_field(obj, "content");
nn->node_type = eg_get_str_field(obj, "node_type");
nn->label = eg_get_str_field(obj, "label");
nn->tier = eg_get_str_field(obj, "tier");
nn->tags = eg_get_str_field(obj, "tags");
nn->metadata = eg_get_str_field(obj, "metadata");
if (!nn->metadata || !*nn->metadata) { free(nn->metadata); nn->metadata = strdup("{}"); }
nn->salience = eg_get_num_field(obj, "salience");
nn->importance = eg_get_num_field(obj, "importance");
nn->confidence = eg_get_num_field(obj, "confidence");
nn->temporal_decay_rate = eg_get_num_field(obj, "temporal_decay_rate");
nn->activation_count = eg_get_int_field(obj, "activation_count");
nn->last_activated = eg_get_int_field(obj, "last_activated");
nn->created_at = eg_get_int_field(obj, "created_at");
nn->updated_at = eg_get_int_field(obj, "updated_at");
nn->background_activation = eg_get_num_field(obj, "background_activation");
nn->working_memory_weight = eg_get_num_field(obj, "working_memory_weight");
if (!isfinite(nn->working_memory_weight) || nn->working_memory_weight < 0.0 || nn->working_memory_weight > 1.0)
nn->working_memory_weight = 0.0;
nn->suppression_count = (int32_t)eg_get_int_field(obj, "suppression_count");
if (json_find_key(obj, "layer_id")) {
nn->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id");
} else {
nn->layer_id = ENGRAM_LAYER_DEFAULT;
}
g->node_count++;
added_nodes++;
}
free(obj);
nodes_p = end;
nodes_p = eg_skip_ws(nodes_p);
if (*nodes_p == ',') { nodes_p++; nodes_p = eg_skip_ws(nodes_p); }
}
}
}
/* Walk edges array — skip if (from_id, to_id, relation) already present */
const char* edges_p = json_find_key(data, "edges");
if (edges_p) {
edges_p = eg_skip_ws(edges_p);
if (*edges_p == '[') {
edges_p++;
edges_p = eg_skip_ws(edges_p);
while (*edges_p && *edges_p != ']') {
if (*edges_p != '{') { edges_p++; continue; }
const char* end = json_skip_value(edges_p);
size_t n = (size_t)(end - edges_p);
char* obj = malloc(n + 1);
memcpy(obj, edges_p, n); obj[n] = '\0';
char* efrom = eg_get_str_field(obj, "from_id");
char* eto = eg_get_str_field(obj, "to_id");
char* erel = eg_get_str_field(obj, "relation");
int dup = 0;
if (efrom && eto && erel) {
for (int64_t ei = 0; ei < g->edge_count; ei++) {
EngramEdge* ex = &g->edges[ei];
if (ex->from_id && ex->to_id && ex->relation &&
strcmp(ex->from_id, efrom) == 0 &&
strcmp(ex->to_id, eto) == 0 &&
strcmp(ex->relation, erel) == 0) {
dup = 1; break;
}
}
}
if (!dup) {
engram_grow_edges();
EngramEdge* ee = &g->edges[g->edge_count];
memset(ee, 0, sizeof(*ee));
ee->id = eg_get_str_field(obj, "id");
ee->from_id = efrom ? efrom : strdup("");
ee->to_id = eto ? eto : strdup("");
ee->relation = erel ? erel : strdup("");
ee->metadata = eg_get_str_field(obj, "metadata");
if (!ee->metadata || !*ee->metadata) { free(ee->metadata); ee->metadata = strdup("{}"); }
ee->weight = eg_get_num_field(obj, "weight");
ee->confidence = eg_get_num_field(obj, "confidence");
ee->created_at = eg_get_int_field(obj, "created_at");
ee->updated_at = eg_get_int_field(obj, "updated_at");
ee->last_fired = eg_get_int_field(obj, "last_fired");
ee->inhibitory = (int)eg_get_int_field(obj, "inhibitory");
if (json_find_key(obj, "layer_id")) {
ee->layer_id = (uint32_t)eg_get_int_field(obj, "layer_id");
} else {
ee->layer_id = ENGRAM_LAYER_DEFAULT;
}
g->edge_count++;
efrom = NULL; eto = NULL; erel = NULL;
} else {
free(efrom); free(eto); free(erel);
}
free(obj);
edges_p = end;
edges_p = eg_skip_ws(edges_p);
if (*edges_p == ',') { edges_p++; edges_p = eg_skip_ws(edges_p); }
}
}
}
free(data);
return (el_val_t)added_nodes;
}
/* ── Engram JSON-string accessors ─────────────────────────────────────────
* These return pre-serialized JSON strings so callers (especially HTTP
* handlers) don't have to round-trip ElList/ElMap through json_stringify
@@ -7601,95 +7298,6 @@ el_val_t engram_activate_json(el_val_t query, el_val_t depth) {
return el_wrap_str(b.buf);
}
/* ── Working memory introspection helpers ────────────────────────────────────
*
* These three functions give the soul daemon visibility into WM composition
* without re-running activation. Used in heartbeat ISEs and curiosity scans.
* Ported from el-compiler/runtime to releases/v1.0.0-20260501 on 2026-06-30
* self-review (they were missing from the release build, breaking soul daemon
* compilation). */
el_val_t engram_wm_count(void) {
EngramStore* g = engram_get();
int64_t count = 0;
for (int64_t i = 0; i < g->node_count; i++) {
if (g->nodes[i].working_memory_weight > 0.0) count++;
}
return (el_val_t)count;
}
/* Average working_memory_weight across all promoted nodes (wm > 0).
* Returns the float bit-pattern via el_from_float so EL can use it with
* float_to_str / float_gt. Returns 0.0 when no nodes are promoted.
* Useful in heartbeat ISEs to distinguish "many weak activations" from
* "few strong activations". Added 2026-06-04 self-review. */
el_val_t engram_wm_avg_weight(void) {
EngramStore* g = engram_get();
double sum = 0.0;
int64_t count = 0;
for (int64_t i = 0; i < g->node_count; i++) {
double w = g->nodes[i].working_memory_weight;
/* Skip corrupt/out-of-range values so a single bad snapshot node
* doesn't produce a garbage average. */
if (w > 0.0 && w <= 1.0 && isfinite(w)) { sum += w; count++; }
}
double avg = (count > 0) ? (sum / (double)count) : 0.0;
return el_from_float(avg);
}
/* engram_wm_top_json — return top N working-memory nodes (by wm weight) as a
* compact JSON array for ISE heartbeat reporting.
* Each element: {"label":"...","node_type":"...","tier":"...","wm":0.42}
* InternalStateEvent nodes are excluded they're observation artifacts that
* would bury substantive WM content. Added 2026-06-05 self-review. */
el_val_t engram_wm_top_json(el_val_t n_v) {
int64_t top_n = (int64_t)n_v;
if (top_n <= 0) top_n = 10;
if (top_n > 50) top_n = 50;
EngramStore* g = engram_get();
int64_t* idx = malloc((size_t)(g->node_count + 1) * sizeof(int64_t));
if (!idx) return el_wrap_str(el_strdup("[]"));
int64_t mc = 0;
for (int64_t i = 0; i < g->node_count; i++) {
if (g->nodes[i].working_memory_weight > 0.0) {
const char* nt = g->nodes[i].node_type;
if (nt && strcmp(nt, "InternalStateEvent") == 0) continue;
idx[mc++] = i;
}
}
/* Insertion-sort descending by wm weight (mc is typically small). */
for (int64_t i = 1; i < mc; i++) {
int64_t key = idx[i];
double kw = g->nodes[key].working_memory_weight;
int64_t j = i;
while (j > 0 && g->nodes[idx[j-1]].working_memory_weight < kw) {
idx[j] = idx[j-1]; j--;
}
idx[j] = key;
}
int64_t emit = mc < top_n ? mc : top_n;
JsonBuf b; jb_init(&b);
jb_putc(&b, '[');
for (int64_t k = 0; k < emit; k++) {
EngramNode* n = &g->nodes[idx[k]];
if (k > 0) jb_putc(&b, ',');
jb_putc(&b, '{');
jb_puts(&b, "\"label\":");
jb_emit_escaped(&b, n->label ? n->label : "");
jb_puts(&b, ",\"node_type\":");
jb_emit_escaped(&b, n->node_type ? n->node_type : "");
jb_puts(&b, ",\"tier\":");
jb_emit_escaped(&b, n->tier ? n->tier : "");
char tmp[48];
snprintf(tmp, sizeof(tmp), ",\"wm\":%.3f", n->working_memory_weight);
jb_puts(&b, tmp);
jb_putc(&b, '}');
}
free(idx);
jb_putc(&b, ']');
return el_wrap_str(b.buf);
}
el_val_t engram_stats_json(void) {
EngramStore* g = engram_get();
char buf[128];
@@ -601,13 +601,6 @@ el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t d
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
el_val_t engram_stats_json(void);
el_val_t engram_list_layers_json(void);
/* Working memory introspection — count, mean weight, and top-N snapshot.
* Ported from el-compiler/runtime on 2026-06-30 self-review. */
el_val_t engram_wm_count(void);
el_val_t engram_wm_avg_weight(void);
el_val_t engram_wm_top_json(el_val_t n);
/* Merge-load: add nodes/edges from a snapshot without resetting the store. */
el_val_t engram_load_merge(el_val_t path);
/* engram_compile_layered_json — produce a prompt-ready text block split
* into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire)
* and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if