From 7351fb0a8d0d10a63fa26906ae8e990b6fd33361 Mon Sep 17 00:00:00 2001 From: bigmerge Date: Sat, 15 Aug 2026 19:56:43 -0500 Subject: [PATCH] runtime: restore engram_recall_json + cgi_* accessors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit neuron's soul calls engram_recall_json (neuron-api.el:618, memory.el:80) and cgi_principal (studio.el:72). Both existed in the runtime neuron vendored (v1.0.0-20260501) and were absent here, so the soul could not link against current el at all. The dangerous part is what the obvious "fix" would have done. These look like redundant wrappers over one impl: engram_search_json(q, limit) -> eg_search_json_impl(q, limit, 0) LEXICAL engram_recall_json(q, limit) -> eg_search_json_impl(q, limit, 1) SEMANTIC They are not interchangeable, and the split is documented at neuron-api.el:613: search stays LEXICAL because ~40 internal call sites pass a KEY and seven of them DELETE every record returned. Point those at a semantic matcher and they delete fuzzy matches. Conversely, pointing recall at search silently downgrades the mind's entire retrieval surface from semantic to lexical — no error, just permanently worse recall. Implemented over engram_activate(), which in this runtime already IS the semantic path the old with_legs=1 branch built by hand (embeds the query via eg_embed_fetch, scores by cosine, then spreads activation one hop). Output shape matches engram_search_json — a flat array via engram_emit_node_json — because callers parse search's shape, not activate's envelope. Verified: neuron's soul now compiles and links against current el, boots, and serves /health with layers initialized. NOTE for follow-up: current el also ships engram_retrieve_geometric_json, a structure-first retrieval that appears to be the intended successor to recall. Repointing the two recall call sites at it may well be the right end state and would remove the two-wrapper shape entirely — but that is a behavioral change that must be measured against neuron/tools/retrieval-eval/'s gold set, not assumed. This commit preserves existing behavior exactly; it does not decide that question. --- lang/runtime/el_runtime.c | 58 +++++++++++++++++++++++++++++++++++++++ lang/runtime/el_runtime.h | 9 ++++++ 2 files changed, 67 insertions(+) diff --git a/lang/runtime/el_runtime.c b/lang/runtime/el_runtime.c index a9ae6e3..d73155a 100644 --- a/lang/runtime/el_runtime.c +++ b/lang/runtime/el_runtime.c @@ -18271,3 +18271,61 @@ el_val_t el_mem_check(void) { } return 0; } + +/* ── engram_recall_json / cgi_* accessors — restored 2026-08-15 ────────────── + * + * These existed in the runtime neuron vendored (v1.0.0-20260501) and were lost + * when this runtime moved on, so a soul built against current el would fail to + * link — and, worse, the naive "fix" of pointing recall at engram_search_json + * would have SILENTLY DOWNGRADED the mind's whole retrieval surface from + * semantic to lexical, with no error at any layer. + * + * The lexical/semantic split is a real safety boundary, not redundant naming + * (neuron-api.el:613 documents it): engram_search_json stays LEXICAL because + * ~40 internal call sites pass a KEY and seven of them DELETE every record + * returned — making those semantic would delete fuzzy matches. recall is the + * SEMANTIC surface, used by the retrieval routes. + * + * The old implementation was eg_search_json_impl(q, limit, with_legs=1): embed + * the query, cosine over the corpus, then a graph leg from semantic seeds. + * In this runtime that is exactly what engram_activate() already does (it + * embeds via eg_embed_fetch, scores by cosine, then spreads activation), so + * recall delegates to it rather than re-deriving a second semantic path. + * Output shape matches engram_search_json — a flat array of node objects via + * engram_emit_node_json — because existing callers (memory.el:80, + * neuron-api.el:618) parse it as search's shape, not activate's envelope. + * ──────────────────────────────────────────────────────────────────────────── */ + +el_val_t engram_recall_json(el_val_t query, el_val_t limit) { + int64_t lim = (int64_t)limit; + if (lim <= 0) lim = 100; + + /* depth 1: the associative leg, one hop out from the semantic seeds. */ + el_val_t lst = engram_activate(query, (el_val_t)(int64_t)1); + ElList* arr = (ElList*)(uintptr_t)lst; + + JsonBuf b; jb_init(&b); + jb_putc(&b, '['); + int64_t emitted = 0; + if (arr) { + for (int64_t i = 0; i < arr->length && emitted < lim; i++) { + if (!arr->elems[i]) continue; + el_val_t node_map = el_map_get(arr->elems[i], EL_STR("node")); + el_val_t id_v = el_map_get(node_map, EL_STR("id")); + const char* id_s = EL_CSTR(id_v); + EngramNode* n = id_s ? engram_find_node(id_s) : NULL; + if (!n) continue; + if (emitted > 0) jb_putc(&b, ','); + engram_emit_node_json(&b, n, 0); + emitted++; + } + } + jb_putc(&b, ']'); + return el_wrap_str(b.buf); +} + +/* cgi_* — read-only identity accessors over the process-wide CGI registration + * set by cgi_register(). Read-only by design: there is no setter (studio.el:66). */ +el_val_t cgi_principal(void) { return EL_STR(_el_cgi_principal ? _el_cgi_principal : ""); } +el_val_t cgi_network(void) { return EL_STR(_el_cgi_network ? _el_cgi_network : ""); } +el_val_t cgi_engram(void) { return EL_STR(_el_cgi_engram ? _el_cgi_engram : ""); } diff --git a/lang/runtime/el_runtime.h b/lang/runtime/el_runtime.h index 7a445b2..c7a3c11 100644 --- a/lang/runtime/el_runtime.h +++ b/lang/runtime/el_runtime.h @@ -1017,6 +1017,15 @@ el_val_t stdout_to_file(el_val_t path); el_val_t stdout_restore(void); el_val_t el_mem_check(void); +/* Semantic retrieval surface. NOT interchangeable with engram_search_json, + * which is lexical by design — see the note at the definition. */ +el_val_t engram_recall_json(el_val_t query, el_val_t limit); + +/* CGI identity accessors (read-only). */ +el_val_t cgi_principal(void); +el_val_t cgi_network(void); +el_val_t cgi_engram(void); + #ifdef __cplusplus } #endif