|
|
|
@@ -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
|
|
|
|
@@ -182,6 +190,7 @@ el_val_t println(el_val_t s) {
|
|
|
|
|
const char* str = EL_CSTR(s);
|
|
|
|
|
if (str) puts(str);
|
|
|
|
|
else puts("");
|
|
|
|
|
fflush(stdout); /* prevent startup logs from silently buffering when stdout→file */
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -701,6 +710,7 @@ static void jb_init(JsonBuf* b);
|
|
|
|
|
static void jb_putc(JsonBuf* b, char c);
|
|
|
|
|
static void jb_puts(JsonBuf* b, const char* s);
|
|
|
|
|
static void jb_emit_escaped(JsonBuf* b, const char* s);
|
|
|
|
|
static char* jb_finish(JsonBuf* b);
|
|
|
|
|
static int looks_like_string(el_val_t v);
|
|
|
|
|
static const char* json_find_key(const char* s, const char* key);
|
|
|
|
|
static const char* json_skip_value(const char* p);
|
|
|
|
@@ -1054,7 +1064,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 +1538,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 +1583,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 +1605,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 +1636,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 +1649,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 +1670,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 +1805,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 +1839,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 +1861,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 +1892,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 +1905,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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1988,7 +2015,7 @@ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) {
|
|
|
|
|
jb_puts(&out, ",\"body\":");
|
|
|
|
|
jb_emit_escaped(&out, b);
|
|
|
|
|
jb_putc(&out, '}');
|
|
|
|
|
return el_wrap_str(out.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&out));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Filesystem ──────────────────────────────────────────────────────────── */
|
|
|
|
@@ -2095,7 +2122,7 @@ el_val_t exec_capture(el_val_t cmdv) {
|
|
|
|
|
char buf[4096];
|
|
|
|
|
while (fgets(buf, sizeof(buf), f)) jb_puts(&b, buf);
|
|
|
|
|
pclose(f);
|
|
|
|
|
return el_wrap_str(b.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&b));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// exec — run a shell command via /bin/sh, capture stdout, return as String.
|
|
|
|
@@ -2119,7 +2146,7 @@ el_val_t exec(el_val_t cmdv) {
|
|
|
|
|
jb_puts(&b, buf);
|
|
|
|
|
}
|
|
|
|
|
pclose(f);
|
|
|
|
|
return el_wrap_str(b.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&b));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// exec_bg — run a shell command in background, return PID as String.
|
|
|
|
@@ -2129,6 +2156,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 +2195,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) {
|
|
|
|
@@ -3248,6 +3293,23 @@ static void jb_puts(JsonBuf* b, const char* s) {
|
|
|
|
|
b->buf[b->len] = '\0';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* jb_finish — call exactly once, at the point a JsonBuf's buffer is handed
|
|
|
|
|
* off as an el_val_t return value (via el_wrap_str). JsonBuf allocates through
|
|
|
|
|
* raw malloc/realloc (jb_init/jb_reserve), NOT el_strdup/el_strbuf, so unlike
|
|
|
|
|
* those helpers it was never registered with the per-request arena — every
|
|
|
|
|
* JsonBuf-built JSON string leaked unconditionally, whether or not a request
|
|
|
|
|
* arena was active, because nothing ever freed it. el_arena_track() is a
|
|
|
|
|
* documented no-op when no arena is active, so this is safe to call from any
|
|
|
|
|
* context (HTTP request handler, CLI arena-scope, or no arena at all — the
|
|
|
|
|
* last case still leaks, same as before, until the caller's context is itself
|
|
|
|
|
* arena-scoped). Do NOT call this on a JsonBuf whose ->buf is consumed by
|
|
|
|
|
* another builder (jb_puts(&other, x.buf)) and freed manually — only on the
|
|
|
|
|
* one actually returned via el_wrap_str. */
|
|
|
|
|
static char* jb_finish(JsonBuf* b) {
|
|
|
|
|
el_arena_track(b->buf);
|
|
|
|
|
return b->buf;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void jb_emit_escaped(JsonBuf* b, const char* s) {
|
|
|
|
|
jb_putc(b, '"');
|
|
|
|
|
const unsigned char* p = (const unsigned char*)s;
|
|
|
|
@@ -3356,7 +3418,7 @@ static void jb_emit_value(JsonBuf* b, el_val_t v) {
|
|
|
|
|
el_val_t json_stringify(el_val_t v) {
|
|
|
|
|
JsonBuf b; jb_init(&b);
|
|
|
|
|
jb_emit_value(&b, v);
|
|
|
|
|
return el_wrap_str(b.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&b));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── JSON substring accessors ────────────────────────────────────────────── */
|
|
|
|
@@ -3543,7 +3605,7 @@ el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value) {
|
|
|
|
|
jb_putc(&b, ':');
|
|
|
|
|
jb_puts(&b, raw_val);
|
|
|
|
|
jb_putc(&b, '}');
|
|
|
|
|
return el_wrap_str(b.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&b));
|
|
|
|
|
}
|
|
|
|
|
const char* existing = json_find_key(json, k);
|
|
|
|
|
JsonBuf b; jb_init(&b);
|
|
|
|
@@ -3557,7 +3619,7 @@ el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value) {
|
|
|
|
|
b.buf[b.len] = '\0';
|
|
|
|
|
jb_puts(&b, raw_val);
|
|
|
|
|
jb_puts(&b, end);
|
|
|
|
|
return el_wrap_str(b.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&b));
|
|
|
|
|
}
|
|
|
|
|
/* Insert before closing '}'. Find last '}' */
|
|
|
|
|
size_t jl = strlen(json);
|
|
|
|
@@ -3589,7 +3651,7 @@ el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value) {
|
|
|
|
|
jb_puts(&b, raw_val);
|
|
|
|
|
/* Append from close_idx onward */
|
|
|
|
|
jb_puts(&b, json + close_idx);
|
|
|
|
|
return el_wrap_str(b.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&b));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
el_val_t json_array_len(el_val_t json_str) {
|
|
|
|
@@ -3727,7 +3789,7 @@ el_val_t json_build_object(el_val_t kvs) {
|
|
|
|
|
jb_putc(&b, '"');
|
|
|
|
|
}
|
|
|
|
|
jb_putc(&b, '}');
|
|
|
|
|
return el_wrap_str(b.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&b));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* json_build_array — build a JSON array from a list of raw JSON values.
|
|
|
|
@@ -3745,7 +3807,7 @@ el_val_t json_build_array(el_val_t items) {
|
|
|
|
|
jb_puts(&b, vs);
|
|
|
|
|
}
|
|
|
|
|
jb_putc(&b, ']');
|
|
|
|
|
return el_wrap_str(b.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&b));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Time ────────────────────────────────────────────────────────────────── */
|
|
|
|
@@ -4478,7 +4540,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);
|
|
|
|
@@ -5144,7 +5211,7 @@ el_val_t state_keys(void) {
|
|
|
|
|
}
|
|
|
|
|
jb_putc(&b, ']');
|
|
|
|
|
pthread_mutex_unlock(&_state_mu);
|
|
|
|
|
return el_wrap_str(b.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&b));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Returns 1 (true) if the key is present in the state store, else 0 (false). */
|
|
|
|
@@ -5350,7 +5417,7 @@ el_val_t str_format(el_val_t fmt, el_val_t data) {
|
|
|
|
|
jb_putc(&b, *p);
|
|
|
|
|
p++;
|
|
|
|
|
}
|
|
|
|
|
return el_wrap_str(b.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&b));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
el_val_t str_lower(el_val_t s) { return str_to_lower(s); }
|
|
|
|
@@ -5766,7 +5833,7 @@ el_val_t list_join(el_val_t listv, el_val_t sep) {
|
|
|
|
|
jb_puts(&b, tmp);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return el_wrap_str(b.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&b));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
el_val_t list_range(el_val_t start, el_val_t end) {
|
|
|
|
@@ -5832,6 +5899,10 @@ el_val_t getpid_now(void) {
|
|
|
|
|
* Returns 0 always (the only non-return path is the exit() branch).
|
|
|
|
|
*/
|
|
|
|
|
el_val_t el_mem_check(void) {
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
/* getrusage is POSIX-only — memory guard disabled on Windows. */
|
|
|
|
|
return 0;
|
|
|
|
|
#else
|
|
|
|
|
/* Read limit from env; default 512 MB. */
|
|
|
|
|
long limit_mb = 512;
|
|
|
|
|
const char *env_val = getenv("ELC_MAX_MEM_MB");
|
|
|
|
@@ -5857,6 +5928,7 @@ el_val_t el_mem_check(void) {
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── args() — command-line argument access ──────────────────────────────────
|
|
|
|
@@ -6031,6 +6103,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
|
|
|
|
@@ -6050,6 +6130,10 @@ static double engram_type_threshold(const char* node_type, const char* tier) {
|
|
|
|
|
if (strcmp(tier, "Lesson") == 0) return 0.25;
|
|
|
|
|
}
|
|
|
|
|
if (node_type) {
|
|
|
|
|
/* Knowledge nodes: Canonical/Lesson handled by tier checks above.
|
|
|
|
|
* Procedural-tier Knowledge (activation_count>=50 migration): 0.20.
|
|
|
|
|
* (2026-06-29 self-review — mirrors release runtime fix) */
|
|
|
|
|
if (strcmp(node_type, "Knowledge") == 0) return 0.20;
|
|
|
|
|
if (strcmp(node_type, "Belief") == 0) return 0.30;
|
|
|
|
|
if (strcmp(node_type, "Entity") == 0) return 0.30;
|
|
|
|
|
}
|
|
|
|
@@ -6208,6 +6292,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 +6420,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. */
|
|
|
|
@@ -6383,12 +6483,12 @@ el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience) {
|
|
|
|
|
n->id = engram_new_id();
|
|
|
|
|
const char* c = EL_CSTR(content);
|
|
|
|
|
const char* nt = EL_CSTR(node_type);
|
|
|
|
|
n->content = el_strdup(c ? c : "");
|
|
|
|
|
n->node_type = el_strdup(nt && *nt ? nt : "Memory");
|
|
|
|
|
n->label = engram_first_n_chars(c, 60);
|
|
|
|
|
n->tier = el_strdup("Working");
|
|
|
|
|
n->tags = el_strdup("");
|
|
|
|
|
n->metadata = el_strdup("{}");
|
|
|
|
|
n->content = el_strdup_persist(c ? c : "");
|
|
|
|
|
n->node_type = el_strdup_persist(nt && *nt ? nt : "Memory");
|
|
|
|
|
{ char* _lb = engram_first_n_chars(c, 60); n->label = el_strdup_persist(_lb); } /* persist: stored field must outlive request arena */
|
|
|
|
|
n->tier = el_strdup_persist("Working");
|
|
|
|
|
n->tags = el_strdup_persist("");
|
|
|
|
|
n->metadata = el_strdup_persist("{}");
|
|
|
|
|
n->salience = engram_decode_score(salience);
|
|
|
|
|
if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5;
|
|
|
|
|
n->importance = 0.5;
|
|
|
|
@@ -6399,11 +6499,44 @@ 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));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* engram_is_valid_utf8 — return 1 if s is valid UTF-8, 0 if it contains invalid bytes.
|
|
|
|
|
* Rejects overlong encodings, surrogate halves, and byte sequences > 4 bytes. */
|
|
|
|
|
static int engram_is_valid_utf8(const char* s) {
|
|
|
|
|
if (!s) return 1;
|
|
|
|
|
const unsigned char* p = (const unsigned char*)s;
|
|
|
|
|
while (*p) {
|
|
|
|
|
if (*p < 0x80) {
|
|
|
|
|
/* ASCII */
|
|
|
|
|
p++;
|
|
|
|
|
} else if ((*p & 0xE0) == 0xC0) {
|
|
|
|
|
/* 2-byte sequence */
|
|
|
|
|
if ((p[1] & 0xC0) != 0x80) return 0;
|
|
|
|
|
if ((*p & 0xFE) == 0xC0) return 0; /* overlong */
|
|
|
|
|
p += 2;
|
|
|
|
|
} else if ((*p & 0xF0) == 0xE0) {
|
|
|
|
|
/* 3-byte sequence */
|
|
|
|
|
if ((p[1] & 0xC0) != 0x80 || (p[2] & 0xC0) != 0x80) return 0;
|
|
|
|
|
if (*p == 0xE0 && (p[1] & 0xE0) == 0x80) return 0; /* overlong */
|
|
|
|
|
if (*p == 0xED && (p[1] & 0xE0) == 0xA0) return 0; /* surrogate */
|
|
|
|
|
p += 3;
|
|
|
|
|
} else if ((*p & 0xF8) == 0xF0) {
|
|
|
|
|
/* 4-byte sequence */
|
|
|
|
|
if ((p[1] & 0xC0) != 0x80 || (p[2] & 0xC0) != 0x80 || (p[3] & 0xC0) != 0x80) return 0;
|
|
|
|
|
if (*p == 0xF0 && (p[1] & 0xF0) == 0x80) return 0; /* overlong */
|
|
|
|
|
if (*p > 0xF4) return 0; /* above U+10FFFF */
|
|
|
|
|
p += 4;
|
|
|
|
|
} else {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label,
|
|
|
|
|
el_val_t salience, el_val_t importance, el_val_t confidence,
|
|
|
|
|
el_val_t tier, el_val_t tags) {
|
|
|
|
@@ -6417,12 +6550,24 @@ 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("{}");
|
|
|
|
|
/* UTF-8 guard: reject content with invalid UTF-8 bytes. Persisting invalid
|
|
|
|
|
* UTF-8 garbles JSON snapshots and corrupts every subsequent node read. */
|
|
|
|
|
if (c && !engram_is_valid_utf8(c)) {
|
|
|
|
|
fprintf(stderr, "[engram] REJECTED node write — content contains invalid UTF-8 (label=%s)\n",
|
|
|
|
|
lb ? lb : "(null)");
|
|
|
|
|
return EL_STR("");
|
|
|
|
|
}
|
|
|
|
|
/* 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 +6580,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));
|
|
|
|
|
}
|
|
|
|
@@ -6470,11 +6615,11 @@ el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t labe
|
|
|
|
|
const char* lb = EL_CSTR(label);
|
|
|
|
|
const char* tg = EL_CSTR(tags);
|
|
|
|
|
const char* st = EL_CSTR(status);
|
|
|
|
|
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("Working");
|
|
|
|
|
n->tags = el_strdup(tg ? tg : "");
|
|
|
|
|
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("Working");
|
|
|
|
|
n->tags = el_strdup_persist(tg ? tg : "");
|
|
|
|
|
if (st && *st) {
|
|
|
|
|
/* Minimal metadata payload: {"status":"..."}. Keep it cheap so
|
|
|
|
|
* callers using `status` don't pay JSON parse cost on every read. */
|
|
|
|
@@ -6483,7 +6628,7 @@ el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t labe
|
|
|
|
|
snprintf(meta, sl, "{\"status\":\"%s\"}", st);
|
|
|
|
|
n->metadata = meta;
|
|
|
|
|
} else {
|
|
|
|
|
n->metadata = el_strdup("{}");
|
|
|
|
|
n->metadata = el_strdup_persist("{}");
|
|
|
|
|
}
|
|
|
|
|
n->salience = engram_decode_score(salience);
|
|
|
|
|
n->importance = engram_decode_score(certainty);
|
|
|
|
@@ -6756,10 +6901,10 @@ void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t
|
|
|
|
|
EngramEdge* e = &g->edges[g->edge_count];
|
|
|
|
|
memset(e, 0, sizeof(*e));
|
|
|
|
|
e->id = engram_new_id();
|
|
|
|
|
e->from_id = el_strdup(f);
|
|
|
|
|
e->to_id = el_strdup(t);
|
|
|
|
|
e->relation = el_strdup(r && *r ? r : "associate");
|
|
|
|
|
e->metadata = el_strdup("{}");
|
|
|
|
|
e->from_id = el_strdup_persist(f);
|
|
|
|
|
e->to_id = el_strdup_persist(t);
|
|
|
|
|
e->relation = el_strdup_persist(r && *r ? r : "associate");
|
|
|
|
|
e->metadata = el_strdup_persist("{}");
|
|
|
|
|
e->weight = engram_decode_score(weight);
|
|
|
|
|
if (e->weight <= 0.0 || e->weight > 1.0) e->weight = 0.5;
|
|
|
|
|
e->confidence = 1.0;
|
|
|
|
@@ -7365,23 +7510,38 @@ 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. */
|
|
|
|
|
static char* eg_get_str_field(const char* obj, const char* key) {
|
|
|
|
|
const char* p = json_find_key(obj, key);
|
|
|
|
|
if (!p) return el_strdup("");
|
|
|
|
|
if (*p != '"') return el_strdup("");
|
|
|
|
|
if (!p) return el_strdup_persist("");
|
|
|
|
|
if (*p != '"') return el_strdup_persist("");
|
|
|
|
|
JsonParser jp = { .p = p, .end = p + strlen(p), .err = 0 };
|
|
|
|
|
char* out = jp_parse_string_raw(&jp);
|
|
|
|
|
if (jp.err) { free(out); return el_strdup(""); }
|
|
|
|
|
if (jp.err) { free(out); return el_strdup_persist(""); }
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -7455,7 +7615,7 @@ el_val_t engram_load(el_val_t path) {
|
|
|
|
|
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 = el_strdup("{}"); }
|
|
|
|
|
if (!nn->metadata || !*nn->metadata) { free(nn->metadata); nn->metadata = el_strdup_persist("{}"); }
|
|
|
|
|
nn->salience = eg_get_num_field(obj, "salience");
|
|
|
|
|
nn->importance = eg_get_num_field(obj, "importance");
|
|
|
|
|
nn->confidence = eg_get_num_field(obj, "confidence");
|
|
|
|
@@ -7506,7 +7666,7 @@ el_val_t engram_load(el_val_t path) {
|
|
|
|
|
ee->to_id = eg_get_str_field(obj, "to_id");
|
|
|
|
|
ee->relation = eg_get_str_field(obj, "relation");
|
|
|
|
|
ee->metadata = eg_get_str_field(obj, "metadata");
|
|
|
|
|
if (!ee->metadata || !*ee->metadata) { free(ee->metadata); ee->metadata = el_strdup("{}"); }
|
|
|
|
|
if (!ee->metadata || !*ee->metadata) { free(ee->metadata); ee->metadata = el_strdup_persist("{}"); }
|
|
|
|
|
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");
|
|
|
|
@@ -7598,7 +7758,7 @@ el_val_t engram_get_node_json(el_val_t id) {
|
|
|
|
|
if (!n) return el_wrap_str(el_strdup("{}"));
|
|
|
|
|
JsonBuf b; jb_init(&b);
|
|
|
|
|
engram_emit_node_json(&b, n);
|
|
|
|
|
return el_wrap_str(b.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&b));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
el_val_t engram_search_json(el_val_t query, el_val_t limit) {
|
|
|
|
@@ -7626,7 +7786,7 @@ el_val_t engram_search_json(el_val_t query, el_val_t limit) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
jb_putc(&b, ']');
|
|
|
|
|
return el_wrap_str(b.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&b));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset) {
|
|
|
|
@@ -7635,9 +7795,9 @@ el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset) {
|
|
|
|
|
int64_t off = (int64_t)offset; if (off < 0) off = 0;
|
|
|
|
|
JsonBuf b; jb_init(&b);
|
|
|
|
|
jb_putc(&b, '[');
|
|
|
|
|
if (g->node_count == 0) { jb_putc(&b, ']'); return el_wrap_str(b.buf); }
|
|
|
|
|
if (g->node_count == 0) { jb_putc(&b, ']'); return el_wrap_str(jb_finish(&b)); }
|
|
|
|
|
int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t));
|
|
|
|
|
if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); }
|
|
|
|
|
if (!idx) { jb_putc(&b, ']'); return el_wrap_str(jb_finish(&b)); }
|
|
|
|
|
/* Skip transparent layers — introspection filter, same as engram_scan_nodes. */
|
|
|
|
|
int64_t live = 0;
|
|
|
|
|
for (int64_t i = 0; i < g->node_count; i++) {
|
|
|
|
@@ -7655,7 +7815,7 @@ el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset) {
|
|
|
|
|
}
|
|
|
|
|
free(idx);
|
|
|
|
|
jb_putc(&b, ']');
|
|
|
|
|
return el_wrap_str(b.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&b));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* engram_scan_nodes_by_type_json — filter by node_type before paginating.
|
|
|
|
@@ -7671,9 +7831,9 @@ el_val_t engram_scan_nodes_by_type_json(el_val_t type_v, el_val_t limit, el_val_
|
|
|
|
|
int64_t off = (int64_t)offset; if (off < 0) off = 0;
|
|
|
|
|
JsonBuf b; jb_init(&b);
|
|
|
|
|
jb_putc(&b, '[');
|
|
|
|
|
if (g->node_count == 0) { jb_putc(&b, ']'); return el_wrap_str(b.buf); }
|
|
|
|
|
if (g->node_count == 0) { jb_putc(&b, ']'); return el_wrap_str(jb_finish(&b)); }
|
|
|
|
|
int64_t* idx = malloc((size_t)g->node_count * sizeof(int64_t));
|
|
|
|
|
if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); }
|
|
|
|
|
if (!idx) { jb_putc(&b, ']'); return el_wrap_str(jb_finish(&b)); }
|
|
|
|
|
int64_t live = 0;
|
|
|
|
|
for (int64_t i = 0; i < g->node_count; i++) {
|
|
|
|
|
if (engram_layer_is_transparent(g->nodes[i].layer_id)) continue;
|
|
|
|
@@ -7692,7 +7852,7 @@ el_val_t engram_scan_nodes_by_type_json(el_val_t type_v, el_val_t limit, el_val_
|
|
|
|
|
}
|
|
|
|
|
free(idx);
|
|
|
|
|
jb_putc(&b, ']');
|
|
|
|
|
return el_wrap_str(b.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&b));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction) {
|
|
|
|
@@ -7707,7 +7867,7 @@ el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t di
|
|
|
|
|
int allow_in = (strcmp(dir, "in") == 0) || (strcmp(dir, "both") == 0);
|
|
|
|
|
JsonBuf b; jb_init(&b);
|
|
|
|
|
jb_putc(&b, '[');
|
|
|
|
|
if (!sid || !*sid) { jb_putc(&b, ']'); return el_wrap_str(b.buf); }
|
|
|
|
|
if (!sid || !*sid) { jb_putc(&b, ']'); return el_wrap_str(jb_finish(&b)); }
|
|
|
|
|
|
|
|
|
|
/* Frontier of (node_id, hops). Cap to a sane size. */
|
|
|
|
|
char** frontier = calloc(1024, sizeof(char*));
|
|
|
|
@@ -7717,7 +7877,7 @@ el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t di
|
|
|
|
|
int64_t vc = 0;
|
|
|
|
|
if (!frontier || !frontier_h || !visited) {
|
|
|
|
|
free(frontier); free(frontier_h); free(visited);
|
|
|
|
|
jb_putc(&b, ']'); return el_wrap_str(b.buf);
|
|
|
|
|
jb_putc(&b, ']'); return el_wrap_str(jb_finish(&b));
|
|
|
|
|
}
|
|
|
|
|
/* Use plain strdup (not el_strdup) so arena doesn't track these pointers.
|
|
|
|
|
* The BFS loop manually frees them below — arena would double-free them. */
|
|
|
|
@@ -7760,7 +7920,7 @@ el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t di
|
|
|
|
|
for (int64_t i = 0; i < vc; i++) free(visited[i]);
|
|
|
|
|
free(frontier); free(frontier_h); free(visited);
|
|
|
|
|
jb_putc(&b, ']');
|
|
|
|
|
return el_wrap_str(b.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&b));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
el_val_t engram_activate_json(el_val_t query, el_val_t depth) {
|
|
|
|
@@ -7801,7 +7961,7 @@ el_val_t engram_activate_json(el_val_t query, el_val_t depth) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
jb_putc(&b, ']');
|
|
|
|
|
return el_wrap_str(b.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&b));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
el_val_t engram_stats_json(void) {
|
|
|
|
@@ -7821,7 +7981,7 @@ el_val_t engram_list_layers_json(void) {
|
|
|
|
|
jb_putc(&b, '[');
|
|
|
|
|
/* Build a sorted index over live layers. */
|
|
|
|
|
size_t* idx = malloc((g->layer_count + 1) * sizeof(size_t));
|
|
|
|
|
if (!idx) { jb_putc(&b, ']'); return el_wrap_str(b.buf); }
|
|
|
|
|
if (!idx) { jb_putc(&b, ']'); return el_wrap_str(jb_finish(&b)); }
|
|
|
|
|
size_t live = 0;
|
|
|
|
|
for (size_t i = 0; i < g->layer_count; i++) {
|
|
|
|
|
if (g->layers[i].name) idx[live++] = i;
|
|
|
|
@@ -7858,7 +8018,7 @@ el_val_t engram_list_layers_json(void) {
|
|
|
|
|
}
|
|
|
|
|
free(idx);
|
|
|
|
|
jb_putc(&b, ']');
|
|
|
|
|
return el_wrap_str(b.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&b));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* engram_compile_layered_json — produce a prompt-ready context block split
|
|
|
|
@@ -7944,7 +8104,7 @@ el_val_t engram_compile_layered_json(el_val_t intent, el_val_t depth) {
|
|
|
|
|
free(b.buf);
|
|
|
|
|
return el_wrap_str(el_strdup(""));
|
|
|
|
|
}
|
|
|
|
|
return el_wrap_str(b.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&b));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* engram_query_range — temporal range query.
|
|
|
|
@@ -7989,7 +8149,7 @@ el_val_t engram_query_range(el_val_t start_ms_v, el_val_t end_ms_v) {
|
|
|
|
|
}
|
|
|
|
|
jb_putc(&b, ']');
|
|
|
|
|
free(idx);
|
|
|
|
|
return el_wrap_str(b.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&b));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* engram_load_merge — like engram_load but WITHOUT resetting the store.
|
|
|
|
@@ -8240,7 +8400,7 @@ el_val_t engram_wm_top_json(el_val_t n_v) {
|
|
|
|
|
}
|
|
|
|
|
free(idx);
|
|
|
|
|
jb_putc(&b, ']');
|
|
|
|
|
return el_wrap_str(b.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&b));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#ifdef HAVE_CURL
|
|
|
|
@@ -8636,13 +8796,13 @@ static int64_t dharma_find_or_create_relation_edge(const char* peer_base, int cr
|
|
|
|
|
engram_grow_nodes();
|
|
|
|
|
EngramNode* n = &g->nodes[g->node_count];
|
|
|
|
|
memset(n, 0, sizeof(*n));
|
|
|
|
|
n->id = el_strdup(self_id);
|
|
|
|
|
n->content = el_strdup(_el_cgi_dharma_id ? _el_cgi_dharma_id : "(self)");
|
|
|
|
|
n->node_type = el_strdup("DharmaSelf");
|
|
|
|
|
n->label = el_strdup("dharma:self");
|
|
|
|
|
n->tier = el_strdup("Working");
|
|
|
|
|
n->tags = el_strdup("dharma");
|
|
|
|
|
n->metadata = el_strdup("{}");
|
|
|
|
|
n->id = el_strdup_persist(self_id);
|
|
|
|
|
n->content = el_strdup_persist(_el_cgi_dharma_id ? _el_cgi_dharma_id : "(self)");
|
|
|
|
|
n->node_type = el_strdup_persist("DharmaSelf");
|
|
|
|
|
n->label = el_strdup_persist("dharma:self");
|
|
|
|
|
n->tier = el_strdup_persist("Working");
|
|
|
|
|
n->tags = el_strdup_persist("dharma");
|
|
|
|
|
n->metadata = el_strdup_persist("{}");
|
|
|
|
|
n->salience = 1.0; n->importance = 1.0; n->confidence = 1.0;
|
|
|
|
|
int64_t now = engram_now_ms();
|
|
|
|
|
n->created_at = now; n->updated_at = now; n->last_activated = now;
|
|
|
|
@@ -8653,13 +8813,13 @@ static int64_t dharma_find_or_create_relation_edge(const char* peer_base, int cr
|
|
|
|
|
engram_grow_nodes();
|
|
|
|
|
EngramNode* n = &g->nodes[g->node_count];
|
|
|
|
|
memset(n, 0, sizeof(*n));
|
|
|
|
|
n->id = el_strdup(peer_node);
|
|
|
|
|
n->content = el_strdup(peer_base);
|
|
|
|
|
n->node_type = el_strdup("DharmaPeer");
|
|
|
|
|
n->label = el_strdup(peer_node);
|
|
|
|
|
n->tier = el_strdup("Working");
|
|
|
|
|
n->tags = el_strdup("dharma");
|
|
|
|
|
n->metadata = el_strdup("{}");
|
|
|
|
|
n->id = el_strdup_persist(peer_node);
|
|
|
|
|
n->content = el_strdup_persist(peer_base);
|
|
|
|
|
n->node_type = el_strdup_persist("DharmaPeer");
|
|
|
|
|
n->label = el_strdup_persist(peer_node);
|
|
|
|
|
n->tier = el_strdup_persist("Working");
|
|
|
|
|
n->tags = el_strdup_persist("dharma");
|
|
|
|
|
n->metadata = el_strdup_persist("{}");
|
|
|
|
|
n->salience = 0.5; n->importance = 0.5; n->confidence = 1.0;
|
|
|
|
|
int64_t now = engram_now_ms();
|
|
|
|
|
n->created_at = now; n->updated_at = now; n->last_activated = now;
|
|
|
|
@@ -8671,10 +8831,10 @@ static int64_t dharma_find_or_create_relation_edge(const char* peer_base, int cr
|
|
|
|
|
EngramEdge* e = &g->edges[g->edge_count];
|
|
|
|
|
memset(e, 0, sizeof(*e));
|
|
|
|
|
e->id = engram_new_id();
|
|
|
|
|
e->from_id = el_strdup(self_id);
|
|
|
|
|
e->to_id = el_strdup(peer_node);
|
|
|
|
|
e->relation = el_strdup("dharma-relation");
|
|
|
|
|
e->metadata = el_strdup("{}");
|
|
|
|
|
e->from_id = el_strdup_persist(self_id);
|
|
|
|
|
e->to_id = el_strdup_persist(peer_node);
|
|
|
|
|
e->relation = el_strdup_persist("dharma-relation");
|
|
|
|
|
e->metadata = el_strdup_persist("{}");
|
|
|
|
|
e->weight = 0.0;
|
|
|
|
|
e->confidence = 1.0;
|
|
|
|
|
int64_t now = engram_now_ms();
|
|
|
|
@@ -8967,7 +9127,7 @@ static el_val_t llm_extract_text(el_val_t resp_val) {
|
|
|
|
|
free(obj);
|
|
|
|
|
p = end;
|
|
|
|
|
}
|
|
|
|
|
return el_wrap_str(out.buf);
|
|
|
|
|
return el_wrap_str(jb_finish(&out));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
el_val_t llm_call(el_val_t model, el_val_t prompt) {
|
|
|
|
|