Compare commits

..

1 Commits

Author SHA1 Message Date
bigmerge 1dc49b1923 Fix engram search latency: pin embed model, cache query embeddings, bound activate BFS
El SDK CI - dev / build-and-test (pull_request) Failing after 10m1s
Pins the Ollama embed model resident (keep_alive:-1) to avoid multi-second
cold reloads whenever a larger generation model evicts it under unified-
memory pressure (measured cold reload up to ~2.2s vs ~0.02-0.05s warm).

Adds a direct-mapped query-embedding cache (FNV-1a keyed, full strcmp to
reject collisions) so a repeated query costs zero Ollama round-trips —
directly serves the curiosity loop, which reseeds the same query terms
repeatedly.

Replaces engram_activate's unbounded FIFO frontier BFS with a beam-capped,
level-synchronous BFS (default beam 128, tunable via
ENGRAM_ACTIVATE_BEAM) to bound per-hop hub-node explosion that could
previously reach multi-second/crash territory at depth 2-3.

Excludes an inert engram_prune_telemetry build-enabler stub that was only
needed to link this checkout against a newer integration branch — not part
of the fix.
2026-08-15 14:25:12 -05:00
2 changed files with 140 additions and 360 deletions
-312
View File
@@ -371,305 +371,6 @@ fn route_capture_knowledge(method: String, path: String, body: String) -> String
"{\"ok\":true,\"id\":\"" + id + "\"}"
}
//
// THE UNIVERSAL ENGRAM OPERATION reframe_region (native, set-based).
//
// There is ONE operation on the engram: isolate a discrete sub-manifold (a
// REGION) and operate on it AS A WHOLE a set operation:
// isolate (cosine retrieval + adjacency the SET of nodes)
// supersede the stale region as a set (immutable tombstone; originals kept)
// insert the new manifold as a set (dedup/load-merge path)
// rebind edges by cosine
// verify + one atomic persist.
// new = (region superseded) new_manifold.
//
// The SINGLE NODE is the DEGENERATE n=1 case of this SAME operation not a
// separate CRUD path:
// write(content) = reframe(region=, manifold=[1 node]) (route_write)
// supersede(id,new) = reframe(region={id}, manifold=[1 node]) (route_supersede)
// relate(a,b,rel) = the rebind sub-op in isolation (route_create_edge)
// The ONLY anti-pattern is decomposing a region-scale change into a LOOP of
// independent top-level per-node updates. Here the region is the unit: one
// isolate, one atomic set-replace, one persist, one verify iterating members
// INSIDE the one operation is set construction, not the sin.
//
// Spec: knowledge e7a03a94 / f999c5ff. Keystones kn-efeb4a5b / kn-5b606390 are
// write-protected never superseded, never inserted-as identity.
//
fn is_keystone(id: String) -> Bool {
if str_eq(id, "kn-efeb4a5b-5aff-4759-8a97-7233099be6ee") { return true }
if str_eq(id, "kn-5b606390-a52d-4ca2-8e0e-eba141d13440") { return true }
return false
}
// membership test in a [String] set
fn set_has(ids: [String], id: String) -> Bool {
let n: Int = el_list_len(ids)
let i: Int = 0
while i < n {
if str_eq(el_list_get(ids, i), id) { return true }
i = i + 1
}
return false
}
// ISOLATE
// Select the region as a SET: cosine/token retrieval around the vantage
// (aperture k), optionally unioned with the 1-hop adjacency of each hit.
// Keystones are excluded from the mutable region by construction.
fn isolate_region(vantage: String, k: Int, expand: Int) -> [String] {
let ids: [String] = el_list_empty()
if str_eq(vantage, "") { return ids }
// (a) cosine/token retrieval a clean node array [{"id":..},..]
let arr: String = engram_search_json(vantage, k)
let n: Int = json_array_len(arr)
let i: Int = 0
while i < n {
let hit: String = json_array_get(arr, i)
let id: String = json_get_string(hit, "id")
if !str_eq(id, "") {
if !is_keystone(id) {
if !set_has(ids, id) { ids = el_list_append(ids, id) }
}
}
i = i + 1
}
// (b) adjacency: union the 1-hop neighbourhood of each retrieved node.
// Iterate only over the original cosine seeds [0, seeds); neighbours append
// past that bound, so this is one hop, not a transitive sweep.
if expand > 0 {
let seeds: Int = el_list_len(ids)
let s: Int = 0
while s < seeds {
let seed: String = el_list_get(ids, s)
let nb: String = engram_neighbors_json(seed, 1, "both")
let m: Int = json_array_len(nb)
let j: Int = 0
while j < m {
let elem: String = json_array_get(nb, j)
let nodeobj: String = json_get_raw(elem, "node")
let nid: String = json_get_string(nodeobj, "id")
if !str_eq(nid, "") {
if !is_keystone(nid) {
if !set_has(ids, nid) { ids = el_list_append(ids, nid) }
}
}
j = j + 1
}
s = s + 1
}
}
return ids
}
// SUPERSEDE (set)
// Retire the region AS A WHOLE: one region-tombstone marker carries the
// provenance (reason + the full superseded id set); every region node is bound
// to it with a "superseded_by" edge. Originals are RETAINED immutable
// tombstone, never a hard delete (engram_forget is deliberately NOT used).
// Returns the tombstone marker id ("" if the region is empty).
fn supersede_set(region: [String], reason: String) -> String {
let n: Int = el_list_len(region)
if n == 0 { return "" }
let csv: String = ""
let i0: Int = 0
while i0 < n {
let sep: String = if i0 == 0 { "" } else { "," }
csv = csv + sep + el_list_get(region, i0)
i0 = i0 + 1
}
let content: String = "region-tombstone: " + reason + " | superseded " + int_to_str(n) + " nodes: " + csv
let tomb: String = engram_node_full(content, "Tombstone", "region-tombstone", 0.1, 0.1, 1.0, "Episodic", "[\"tombstone\",\"region-supersede\"]")
let i: Int = 0
while i < n {
let rid: String = el_list_get(region, i)
engram_connect(rid, tomb, 1.0, "superseded_by")
i = i + 1
}
return tomb
}
// INSERT (manifold)
// Insert the new manifold as a SET. Inline JSON array of node objects
// {content, node_type?, tier?, tags?}. Each becomes a real embedded node
// (engram_node_full is the n=1 insert atom); the manifold is the set built from
// those atoms, wired with internal "manifold_member" edges so it enters as one
// connected sub-graph. Identity node_types (self/values) are demoted to Memory
// identity can never be minted through reframe. Returns the new node ids.
fn insert_manifold_json(manifold: String) -> [String] {
let out: [String] = el_list_empty()
if str_eq(manifold, "") { return out }
let n: Int = json_array_len(manifold)
if n <= 0 { return out }
let i: Int = 0
let prev: String = ""
while i < n {
let obj: String = json_array_get(manifold, i)
let content: String = json_get_string(obj, "content")
if !str_eq(content, "") {
let nt_raw: String = json_get_string(obj, "node_type")
let nt: String = if str_eq(nt_raw, "") { "Memory" } else { nt_raw }
if str_eq(nt, "self") { nt = "Memory" }
if str_eq(nt, "values") { nt = "Memory" }
let tier_raw: String = json_get_string(obj, "tier")
let tier: String = if str_eq(tier_raw, "") { "Working" } else { tier_raw }
let tags_raw: String = json_get_raw(obj, "tags")
let tags: String = if str_eq(tags_raw, "") { "" } else { tags_raw }
let label: String = str_slice(content, 0, 60)
let id: String = engram_node_full(content, nt, label, 0.5, 0.5, 0.9, tier, tags)
out = el_list_append(out, id)
if !str_eq(prev, "") { engram_connect(prev, id, 0.6, "manifold_member") }
prev = id
}
i = i + 1
}
return out
}
// REBIND (edges by cosine)
// Re-embed the new manifold into the surrounding geometry: bind each new node
// to the tombstone marker (provenance: new region -reframes-> retired region),
// then to its top cosine/token neighbours in the store (skipping itself, the
// new set, keystones, tombstones). Returns the number of edges bound.
fn rebind_cosine(new_ids: [String], tomb: String) -> Int {
let bound: Int = 0
let n: Int = el_list_len(new_ids)
let i: Int = 0
while i < n {
let nid: String = el_list_get(new_ids, i)
if !str_eq(tomb, "") {
engram_connect(nid, tomb, 0.8, "reframes")
bound = bound + 1
}
let node_json: String = engram_get_node_json(nid)
let content: String = json_get_string(node_json, "content")
let arr: String = engram_search_json(content, 5)
let m: Int = json_array_len(arr)
let j: Int = 0
while j < m {
let hit: String = json_array_get(arr, j)
let hid: String = json_get_string(hit, "id")
if !str_eq(hid, "") {
if !str_eq(hid, nid) {
if !is_keystone(hid) {
if !set_has(new_ids, hid) {
let htype: String = json_get_string(hit, "node_type")
if !str_eq(htype, "Tombstone") {
engram_connect(nid, hid, 0.5, "related")
bound = bound + 1
}
}
}
}
}
j = j + 1
}
i = i + 1
}
return bound
}
// THE OPERATION
// isolate (done by caller) supersede region insert manifold rebind
// one atomic persist verify report. This is the whole operation; every
// mutation route below is a projection of it.
fn reframe_core(region: [String], manifold: String, reason: String, do_rebind: Int) -> String {
let n_before: Int = engram_node_count()
let e_before: Int = engram_edge_count()
let region_n: Int = el_list_len(region)
let tomb: String = if region_n > 0 { supersede_set(region, reason) } else { "" }
let new_ids: [String] = insert_manifold_json(manifold)
let inserted: Int = el_list_len(new_ids)
let bound: Int = if do_rebind > 0 { rebind_cosine(new_ids, tomb) } else { 0 }
let saved: Int = persist_canonical()
let new_csv: String = ""
let k: Int = 0
while k < inserted {
let sep: String = if k == 0 { "" } else { "," }
new_csv = new_csv + sep + "\"" + el_list_get(new_ids, k) + "\""
k = k + 1
}
return "{\"ok\":true,\"region_superseded\":" + int_to_str(region_n) +
",\"tombstone_id\":\"" + tomb + "\"" +
",\"inserted\":" + int_to_str(inserted) +
",\"new_ids\":[" + new_csv + "]" +
",\"edges_rebound\":" + int_to_str(bound) +
",\"nodes_added\":" + int_to_str(engram_node_count() - n_before) +
",\"edges_added\":" + int_to_str(engram_edge_count() - e_before) +
",\"node_count\":" + int_to_str(engram_node_count()) +
",\"edge_count\":" + int_to_str(engram_edge_count()) +
",\"keystones_protected\":true}"
}
// POST /api/reframe the universal set-based mutation.
// Body: {vantage?, region_ids?(csv), k?, expand?, manifold(json array), reason?, rebind?}
// region_ids (explicit) wins; else cosine-isolate around vantage.
fn route_reframe(method: String, path: String, body: String) -> String {
let region_csv: String = json_get_string(body, "region_ids")
let vantage: String = json_get_string(body, "vantage")
let region: [String] = el_list_empty()
if !str_eq(region_csv, "") {
let parts: [String] = str_split(region_csv, ",")
let pn: Int = el_list_len(parts)
let i: Int = 0
while i < pn {
let id: String = str_trim(el_list_get(parts, i))
if !str_eq(id, "") {
if is_keystone(id) { return err_json("reframe: identity keystone write-protected") }
if !set_has(region, id) { region = el_list_append(region, id) }
}
i = i + 1
}
} else {
if !str_eq(vantage, "") {
let kv: Int = json_get_int(body, "k")
let kk: Int = if kv > 0 { kv } else { 12 }
let expand: Int = json_get_int(body, "expand")
region = isolate_region(vantage, kk, expand)
}
}
let manifold: String = json_get_raw(body, "manifold")
let reason_raw: String = json_get_string(body, "reason")
let reason: String = if str_eq(reason_raw, "") { "reframe" } else { reason_raw }
// rebind defaults ON for reframe (absent 1); explicit 0 disables.
let rebind_raw: String = json_get_raw(body, "rebind")
let do_rebind: Int = if str_eq(rebind_raw, "") { 1 } else { json_get_int(body, "rebind") }
return reframe_core(region, manifold, reason, do_rebind)
}
// write DEGENERATE n=1 of reframe: region=, manifold=[1 node]. The SAME
// reframe_core path. rebind off so the pure-add matches plain node creation.
// POST /api/write {content, node_type?, tier?, tags?}
fn route_write(method: String, path: String, body: String) -> String {
let content: String = json_get_string(body, "content")
if str_eq(content, "") { return err_json("write: content required") }
let nt: String = json_get_string(body, "node_type")
if str_eq(nt, "self") { return err_json("write: identity is write-protected") }
if str_eq(nt, "values") { return err_json("write: identity is write-protected") }
let empty: [String] = el_list_empty()
let manifold: String = "[" + body + "]" // the body IS a valid manifold node object
return reframe_core(empty, manifold, "write", 0)
}
// supersede DEGENERATE n=1 of reframe: region={id}, manifold=[1 node]. The
// SAME reframe_core path with a size-1 region. Original retained (immutable);
// new node inserted and cosine-rebound; provenance edge new-reframes-tomb.
// POST /api/supersede {id, content, node_type?, tier?, tags?, reason?}
fn route_supersede(method: String, path: String, body: String) -> String {
let id: String = json_get_string(body, "id")
if str_eq(id, "") { return err_json("supersede: id required") }
if is_keystone(id) { return err_json("supersede: identity keystone write-protected") }
let content: String = json_get_string(body, "content")
if str_eq(content, "") { return err_json("supersede: content required") }
let region: [String] = el_list_empty()
region = el_list_append(region, id)
let manifold: String = "[" + body + "]"
let reason_raw: String = json_get_string(body, "reason")
let reason: String = if str_eq(reason_raw, "") { "supersede " + id } else { reason_raw }
return reframe_core(region, manifold, reason, 1)
}
// Auth
fn check_auth_ok(method: String, body: String) -> Bool {
@@ -717,19 +418,6 @@ fn handle_request(method: String, path: String, body: String) -> String {
return route_stats(method, path, body)
}
// The universal set-based operation and its n=1 degenerate projections
// reframe = isolate supersede-region insert-manifold rebind. write and
// supersede are the SAME reframe_core path at region size 0 and 1.
if str_eq(method, "POST") && (str_eq(clean, "/api/reframe") || str_eq(clean, "/reframe")) {
return route_reframe(method, path, body)
}
if str_eq(method, "POST") && (str_eq(clean, "/api/write") || str_eq(clean, "/write")) {
return route_write(method, path, body)
}
if str_eq(method, "POST") && (str_eq(clean, "/api/supersede") || str_eq(clean, "/supersede")) {
return route_supersede(method, path, body)
}
// Nodes
if str_eq(method, "POST") && (str_eq(clean, "/api/nodes") || str_eq(clean, "/nodes")) {
return route_create_node(method, path, body)
+140 -48
View File
@@ -7056,10 +7056,16 @@ static float* engram_embed_raw(const char* prefix, const char* text, int* out_di
char* esc = engram_json_escape(text);
free(trunc);
if (!esc || !esc_prefix) { free(esc); free(esc_prefix); return NULL; }
size_t blen = strlen(esc) + strlen(esc_prefix) + strlen(model) + 64;
size_t blen = strlen(esc) + strlen(esc_prefix) + strlen(model) + 96;
char* body = malloc(blen);
if (!body) { free(esc); free(esc_prefix); return NULL; }
snprintf(body, blen, "{\"model\":\"%s\",\"prompt\":\"%s%s\"}", model, esc_prefix, esc);
/* keep_alive:-1 pins the embed model resident in Ollama indefinitely.
* Without it the tiny embed model is evicted whenever a large generation
* model loads (unified-memory pressure), so the NEXT search pays a cold
* model reload the dominant search-latency cost (measured cold reload
* up to ~2.2s vs ~0.02-0.05s warm). Pinning makes cold reload impossible. */
snprintf(body, blen, "{\"model\":\"%s\",\"keep_alive\":-1,\"prompt\":\"%s%s\"}",
model, esc_prefix, esc);
free(esc); free(esc_prefix);
CURL* c = curl_easy_init();
@@ -7099,11 +7105,52 @@ static int engram_semantic_enabled(void) {
g_emb_state = -1; return 0;
}
/* ── Query-embedding cache ──────────────────────────────────────────────────
* The node embeddings are cached (engram_node_vec) but the QUERY was re-embedded
* on every search/activate call a blocking Ollama round-trip each time. Query
* embeddings are deterministic for a given model, so we cache them keyed by an
* FNV-1a hash of the query string (with a full strcmp to reject hash
* collisions). A repeated query then costs zero network round-trips. This makes
* warm search latency independent of Ollama entirely, and directly serves the
* curiosity loop, which reseeds the same query terms repeatedly. Direct-mapped,
* fixed-size, process-lifetime. */
#define ENGRAM_QCACHE_SIZE 1024
typedef struct { char* q; uint64_t hash; float* vec; int dim; } EngramQCacheEntry;
static EngramQCacheEntry g_qcache[ENGRAM_QCACHE_SIZE];
/* Returns a malloc'd COPY of the cached vector (caller frees), or NULL on miss —
* preserving engram_embed_query's "caller frees" contract. */
static float* engram_qcache_get(const char* q, uint64_t h, int* dim) {
EngramQCacheEntry* e = &g_qcache[h & (ENGRAM_QCACHE_SIZE - 1)];
if (e->vec && e->hash == h && e->q && strcmp(e->q, q) == 0 && e->dim > 0) {
float* copy = malloc((size_t)e->dim * sizeof(float));
if (!copy) return NULL;
memcpy(copy, e->vec, (size_t)e->dim * sizeof(float));
*dim = e->dim; return copy;
}
return NULL;
}
static void engram_qcache_put(const char* q, uint64_t h, const float* vec, int dim) {
if (!vec || dim <= 0) return;
EngramQCacheEntry* e = &g_qcache[h & (ENGRAM_QCACHE_SIZE - 1)];
float* stored = malloc((size_t)dim * sizeof(float));
char* qcopy = el_strdup(q);
if (!stored || !qcopy) { free(stored); free(qcopy); return; }
memcpy(stored, vec, (size_t)dim * sizeof(float));
free(e->q); free(e->vec); /* evict prior occupant of this slot */
e->q = qcopy; e->hash = h; e->vec = stored; e->dim = dim;
}
/* Embed the query. Returns malloc'd vec (caller frees), or NULL if semantic off. */
static float* engram_embed_query(const char* q, int* dim) {
if (!engram_semantic_enabled()) return NULL;
if (!q || !*q) return NULL;
return engram_embed_raw("search_query: ", q, dim);
uint64_t h = engram_fnv1a(q);
float* hit = engram_qcache_get(q, h, dim);
if (hit) return hit;
float* v = engram_embed_raw("search_query: ", q, dim);
if (v && *dim > 0) engram_qcache_put(q, h, v, *dim);
return v;
}
/* Cached node embedding. Returns a pointer OWNED BY THE CACHE — do not free. */
@@ -7537,6 +7584,39 @@ static double engram_goal_bias(const EngramNode* n, const char* query) {
return bias;
}
/* ── Beam cap for engram_activate spreading activation ──────────────────────
* Bounds the number of frontier nodes expanded PER HOP. Without it a single
* high-degree hub enqueues thousands of successors, each re-scanning the whole
* edge list, and dense cycles re-enqueue them repeatedly so capping DEPTH
* does not bound work (measured: depth-2/3 in the multi-second range, depth-3
* can crash). With the cap, only the top-BEAM highest-activation nodes at each
* level spread further. Every reached node is still recorded and returned, so
* recall is preserved the cap bounds only associative spread, never the
* direct seed matches or the reported set. Tunable via ENGRAM_ACTIVATE_BEAM
* (default 128); set very high to restore unbounded behaviour. */
static int64_t engram_activate_beam(void) {
static int64_t v = -1;
if (v >= 0) return v;
const char* s = getenv("ENGRAM_ACTIVATE_BEAM");
int64_t d = 128;
if (s && *s) { char* e = NULL; long t = strtol(s, &e, 10); if (e != s && t > 0) d = (int64_t)t; }
v = d; return v;
}
/* Partition the k highest-`score` entries of idx[0..n) to the front (order
* within the top-k is unspecified). O(k*n) partial selection k is the small
* beam width, so this is cheap relative to a hop's edge scan. */
static void engram_beam_select(int64_t* idx, int64_t n, int64_t k, const double* score) {
if (k >= n) return;
for (int64_t i = 0; i < k; i++) {
int64_t best = i;
for (int64_t j = i + 1; j < n; j++)
if (score[idx[j]] > score[idx[best]]) best = j;
if (best != i) { int64_t t = idx[i]; idx[i] = idx[best]; idx[best] = t; }
}
}
el_val_t engram_activate(el_val_t query, el_val_t depth) {
EngramStore* g = engram_get();
const char* q = EL_CSTR(query);
@@ -7606,53 +7686,65 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
for (int64_t s = 1; s < seed_count; s++)
seed_epoch = (seed_epoch + seeds[s].created_at) / 2;
}
typedef struct { int64_t idx; int64_t hops; double act; } Frontier;
Frontier* fr = malloc((size_t)(g->node_count * (max_depth + 1)) * sizeof(Frontier) + 16 * sizeof(Frontier));
if (!fr) {
/* ── Beam-capped, level-synchronous BFS ────────────────────────────────
* Expand the graph hop-by-hop; at each hop expand only the top-`beam`
* nodes by current best background activation (engram_beam_select). This
* replaces the old unbounded FIFO frontier, which let a hub enqueue
* thousands of successors and dense cycles re-enqueue them without limit
* (the breadth explosion). `reached` / `best_bg` / `best_hops` keep the
* exact same meaning, so the downstream executive/override passes and the
* reported result set are unchanged only how far weak spread propagates
* is bounded. `cur`/`nxt` hold node indices for this/next level; `in_nxt`
* dedups a node to at most one entry per level. */
const int64_t beam = engram_activate_beam();
const double SPREAD_DECAY = 0.7;
int64_t* cur = malloc((size_t)g->node_count * sizeof(int64_t));
int64_t* nxt = malloc((size_t)g->node_count * sizeof(int64_t));
int* in_nxt = calloc((size_t)g->node_count, sizeof(int));
if (!cur || !nxt || !in_nxt) {
free(cur); free(nxt); free(in_nxt);
free(best_bg); free(best_hops); free(reached); free(seeds); return out;
}
int64_t fhead = 0, ftail = 0;
int64_t fcap = (int64_t)((size_t)(g->node_count * (max_depth + 1)) + 16);
for (int64_t s = 0; s < seed_count; s++) {
if (ftail >= fcap) break;
fr[ftail].idx = seeds[s].idx;
fr[ftail].hops = 0;
fr[ftail].act = seeds[s].act;
ftail++;
}
const double SPREAD_DECAY = 0.7;
while (fhead < ftail) {
Frontier f = fr[fhead++];
if (f.hops >= max_depth) continue;
const char* cur_id = g->nodes[f.idx].id;
for (int64_t ei = 0; ei < g->edge_count; ei++) {
EngramEdge* e = &g->edges[ei];
const char* other = NULL;
if (e->from_id && strcmp(e->from_id, cur_id) == 0) other = e->to_id;
else if (e->to_id && strcmp(e->to_id, cur_id) == 0) other = e->from_id;
else continue;
int64_t oi = engram_find_node_index(other);
if (oi < 0) continue;
EngramNode* on = &g->nodes[oi];
double tbonus = engram_temporal_proximity_bonus(on->created_at, seed_epoch);
double tdecay = engram_temporal_decay(on, now_ms);
double dampen = engram_activation_dampen(on);
double new_act = f.act * e->weight * SPREAD_DECAY * (1.0 + tbonus)
* tdecay * dampen;
int64_t new_hops = f.hops + 1;
if (!reached[oi] || new_act > best_bg[oi]) {
best_bg[oi] = new_act;
best_hops[oi] = new_hops;
reached[oi] = 1;
if (ftail < fcap) {
fr[ftail].idx = oi;
fr[ftail].hops = new_hops;
fr[ftail].act = new_act;
ftail++;
int64_t cur_n = 0;
for (int64_t s = 0; s < seed_count && cur_n < g->node_count; s++)
cur[cur_n++] = seeds[s].idx;
for (int64_t hop = 0; hop < max_depth && cur_n > 0; hop++) {
if (cur_n > beam) { engram_beam_select(cur, cur_n, beam, best_bg); cur_n = beam; }
int64_t nxt_n = 0;
for (int64_t ci = 0; ci < cur_n; ci++) {
int64_t fidx = cur[ci];
double f_act = best_bg[fidx];
const char* cur_id = g->nodes[fidx].id;
for (int64_t ei = 0; ei < g->edge_count; ei++) {
EngramEdge* e = &g->edges[ei];
const char* other = NULL;
if (e->from_id && strcmp(e->from_id, cur_id) == 0) other = e->to_id;
else if (e->to_id && strcmp(e->to_id, cur_id) == 0) other = e->from_id;
else continue;
int64_t oi = engram_find_node_index(other);
if (oi < 0) continue;
EngramNode* on = &g->nodes[oi];
double tbonus = engram_temporal_proximity_bonus(on->created_at, seed_epoch);
double tdecay = engram_temporal_decay(on, now_ms);
double dampen = engram_activation_dampen(on);
double new_act = f_act * e->weight * SPREAD_DECAY * (1.0 + tbonus)
* tdecay * dampen;
if (!reached[oi] || new_act > best_bg[oi]) {
best_bg[oi] = new_act;
best_hops[oi] = hop + 1;
reached[oi] = 1;
if (!in_nxt[oi] && nxt_n < g->node_count) {
in_nxt[oi] = 1;
nxt[nxt_n++] = oi;
}
}
}
}
for (int64_t k = 0; k < nxt_n; k++) in_nxt[nxt[k]] = 0;
int64_t* tmp = cur; cur = nxt; nxt = tmp;
cur_n = nxt_n;
}
free(cur); free(nxt); free(in_nxt);
/* Persist layer-1 background_activation to node store. */
for (int64_t i = 0; i < g->node_count; i++) {
g->nodes[i].background_activation = reached[i] ? best_bg[i] : 0.0;
@@ -7666,7 +7758,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
* memory weight cannot be silenced by attentional suppression. */
double* inhibition = calloc((size_t)g->node_count, sizeof(double));
if (!inhibition) {
free(best_bg); free(best_hops); free(reached); free(seeds); free(fr);
free(best_bg); free(best_hops); free(reached); free(seeds);
return out;
}
for (int64_t ei = 0; ei < g->edge_count; ei++) {
@@ -7692,7 +7784,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
double* wm_weights = calloc((size_t)g->node_count, sizeof(double));
if (!wm_weights) {
free(best_bg); free(best_hops); free(reached); free(seeds);
free(fr); free(inhibition); return out;
free(inhibition); return out;
}
for (int64_t i = 0; i < g->node_count; i++) {
if (!reached[i] || best_bg[i] <= 0.0) continue;
@@ -7762,7 +7854,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
int64_t rcount = 0;
if (!results) {
free(best_bg); free(best_hops); free(reached); free(seeds);
free(fr); free(inhibition); free(wm_weights); return out;
free(inhibition); free(wm_weights); return out;
}
for (int64_t i = 0; i < g->node_count; i++) {
if (!reached[i]) continue;
@@ -7806,7 +7898,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
out = el_list_append(out, entry);
}
free(best_bg); free(best_hops); free(reached);
free(seeds); free(fr); free(inhibition); free(wm_weights); free(results);
free(seeds); free(inhibition); free(wm_weights); free(results);
return out;
}