fix(engram): the semantic leg was deleting 234 memory records per boot
The accumulated retrieval stack (iterations 1-9) put the claim-24 semantic
leg and the claim-10 associative leg on engram_search_json — the function
~40 internal .el call sites already used as a KEYED read. Seven of those
sites delete every record that comes back ("prune all existing X nodes,
keep exactly one"): memory.el:176, sessions.el:250/268/444/523,
soul.el:359.
mem_boot_count_inc() calls engram_search_json("soul:boot_count", 50) and
engram_forget()s all 50 results. With a lexical leg that returned 1 record.
With a semantic leg it returns 50 — the 49 nearest neighbours of the STRING
"soul:boot_count" — and the soul deletes them.
MEASURED on the harness corpus, isolated, read-only, zero writes from any
caller: 234 node records destroyed in a single boot. The deletion list is
the soul's own lookup result list, in rank order. Casualties include 6
Knowledge nodes, a layer-1 "CORE IDENTITY - GENESIS, LINEAGE" Memory, the
value node kn-58874a74, and the gold answers to 8 of the 75 gold-set
queries. After the fix: 1 deletion, which is the one the code intends.
THE BOUNDARY, from Will. Claim 24 authorises the vector index "to respond
to EMBEDDING SEARCH QUERIES by returning the node records whose embedding
vectors have the highest cosine similarity to a query vector". A keyed
state read is not an embedding search query; it is the identifier-keyed
retrieval of claim 23 ("node records are stored under a key encoding the
node identifier"). One function served both, so a nearest neighbour of
"soul:boot_count" was treated as a boot counter.
So: engram_search_json returns to its lexical contract, and the legs move
to engram_recall_json, which is what /api/neuron/recall reaches — the route
the MCP wrapper, the app, and this harness all call. Retrieval quality on
that route is unchanged by construction.
MEASURED, 75-query extended gold set, embedded corpus, vs the iteration-9
baseline: +3 / -0 (q15, q28, q60), p=0.2500, hit@5 53.8 -> 58.5%, latency
1.02x, every regression guard held, nonsense 10/10. Net +3 against a floor
of 6 is NOT-SHOWN and I am not calling it an improvement. The deliverable
is the defect.
Diagnostics kept, env-gated (EG_DIAG / EG_DIAG_ID), zero cost when unset:
node/embedding census at load, per-query leg dump, and a FORGET log — the
last is the regression detector for exactly this class of bug.
LIMIT, stated: handle_api_search_knowledge still uses the lexical function.
It is a retrieval surface and arguably wants the legs, but nothing in this
harness measures it, so I did not change unmeasured behaviour.
This commit is contained in:
+6
-1
@@ -450,7 +450,12 @@ fn handle_api_recall(method: String, path: String, body: String) -> String {
|
||||
if str_eq(eff_q, "") {
|
||||
return api_or_empty(engram_scan_nodes_json(limit, 0))
|
||||
}
|
||||
let results: String = engram_search_json(eff_q, limit)
|
||||
// engram_recall_json, not engram_search_json: this route IS the retrieval
|
||||
// surface (claim 24's "embedding search queries"), so it gets the semantic
|
||||
// and associative legs. engram_search_json stays lexical because ~40
|
||||
// internal call sites pass a KEY and seven of them delete every record
|
||||
// that comes back — see the boundary note above eg_search_json_impl.
|
||||
let results: String = engram_recall_json(eff_q, limit)
|
||||
return api_or_empty(results)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
{
|
||||
"baseline": "unfloor-clean",
|
||||
"candidate": "splitfix",
|
||||
"n_shared_queries": 75,
|
||||
"fixed_by_candidate": [
|
||||
"q15",
|
||||
"q28",
|
||||
"q60"
|
||||
],
|
||||
"broken_by_candidate": [],
|
||||
"discordant": 3,
|
||||
"net_queries": 3,
|
||||
"mcnemar_exact_p": 0.25,
|
||||
"min_detectable_swing_queries": 6,
|
||||
"observed_run_to_run_drift_queries": 0,
|
||||
"noise_floor_queries": 6,
|
||||
"verdict": "no measurable difference",
|
||||
"baseline_aggregate": {
|
||||
"n_queries": 75,
|
||||
"n_scored": 65,
|
||||
"hit@5": 0.5384615384615384,
|
||||
"recall@5": 0.44907176157176154,
|
||||
"recall@10": 0.5380300255300255,
|
||||
"precision@5": 0.13230769230769232,
|
||||
"mrr@10": 0.32437728937728944,
|
||||
"nonsense_clean": "10/10",
|
||||
"superseded_outranks": "2/3",
|
||||
"latency_ms_p50": 632.5,
|
||||
"latency_ms_p95": 992.5,
|
||||
"latency_ms_max": 1177.8,
|
||||
"errors": 0,
|
||||
"by_category": {
|
||||
"associative": {
|
||||
"n": 6,
|
||||
"hit@5": 0.5,
|
||||
"recall@5": 0.07575757575757576,
|
||||
"recall@10": 0.13636363636363635,
|
||||
"mrr@10": 0.23214285714285712
|
||||
},
|
||||
"exact_rare": {
|
||||
"n": 6,
|
||||
"hit@5": 1.0,
|
||||
"recall@5": 1.0,
|
||||
"recall@10": 1.0,
|
||||
"mrr@10": 1.0
|
||||
},
|
||||
"heldout_paraphrase": {
|
||||
"n": 30,
|
||||
"hit@5": 0.3,
|
||||
"recall@5": 0.3,
|
||||
"recall@10": 0.4,
|
||||
"mrr@10": 0.11638888888888889
|
||||
},
|
||||
"nonsense": {
|
||||
"n": 10,
|
||||
"clean": 10,
|
||||
"avg_false_positives": 0.0
|
||||
},
|
||||
"paraphrase": {
|
||||
"n": 13,
|
||||
"hit@5": 0.6923076923076923,
|
||||
"recall@5": 0.6923076923076923,
|
||||
"recall@10": 0.7692307692307693,
|
||||
"mrr@10": 0.29423076923076924
|
||||
},
|
||||
"phrase": {
|
||||
"n": 7,
|
||||
"hit@5": 1.0,
|
||||
"recall@5": 0.5335884353741497,
|
||||
"recall@10": 0.5933956916099773,
|
||||
"mrr@10": 0.8214285714285714
|
||||
},
|
||||
"superseded": {
|
||||
"n": 3,
|
||||
"hit@5": 0.3333333333333333,
|
||||
"recall@5": 0.3333333333333333,
|
||||
"recall@10": 0.6666666666666666,
|
||||
"mrr@10": 0.20833333333333334,
|
||||
"outranks": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"candidate_aggregate": {
|
||||
"n_queries": 75,
|
||||
"n_scored": 65,
|
||||
"hit@5": 0.5846153846153846,
|
||||
"recall@5": 0.48102442429365505,
|
||||
"recall@10": 0.5692415490492414,
|
||||
"precision@5": 0.14153846153846153,
|
||||
"mrr@10": 0.3351709401709402,
|
||||
"nonsense_clean": "10/10",
|
||||
"superseded_outranks": "2/3",
|
||||
"latency_ms_p50": 646.9,
|
||||
"latency_ms_p95": 1028.5,
|
||||
"latency_ms_max": 1197.8,
|
||||
"errors": 0,
|
||||
"by_category": {
|
||||
"associative": {
|
||||
"n": 6,
|
||||
"hit@5": 0.6666666666666666,
|
||||
"recall@5": 0.08857808857808858,
|
||||
"recall@10": 0.15967365967365968,
|
||||
"mrr@10": 0.24166666666666667
|
||||
},
|
||||
"exact_rare": {
|
||||
"n": 6,
|
||||
"hit@5": 1.0,
|
||||
"recall@5": 1.0,
|
||||
"recall@10": 1.0,
|
||||
"mrr@10": 1.0
|
||||
},
|
||||
"heldout_paraphrase": {
|
||||
"n": 30,
|
||||
"hit@5": 0.3333333333333333,
|
||||
"recall@5": 0.3333333333333333,
|
||||
"recall@10": 0.43333333333333335,
|
||||
"mrr@10": 0.12120370370370372
|
||||
},
|
||||
"nonsense": {
|
||||
"n": 10,
|
||||
"clean": 10,
|
||||
"avg_false_positives": 0.0
|
||||
},
|
||||
"paraphrase": {
|
||||
"n": 13,
|
||||
"hit@5": 0.7692307692307693,
|
||||
"recall@5": 0.7692307692307693,
|
||||
"recall@10": 0.8461538461538461,
|
||||
"mrr@10": 0.33269230769230773
|
||||
},
|
||||
"phrase": {
|
||||
"n": 7,
|
||||
"hit@5": 1.0,
|
||||
"recall@5": 0.5335884353741497,
|
||||
"recall@10": 0.5775226757369615,
|
||||
"mrr@10": 0.8214285714285714
|
||||
},
|
||||
"superseded": {
|
||||
"n": 3,
|
||||
"hit@5": 0.3333333333333333,
|
||||
"recall@5": 0.3333333333333333,
|
||||
"recall@10": 0.6666666666666666,
|
||||
"mrr@10": 0.20833333333333334,
|
||||
"outranks": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeat_variance": {}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+94
-2
@@ -7175,6 +7175,12 @@ void engram_forget(el_val_t node_id) {
|
||||
if (idx < 0) return;
|
||||
/* Free node strings */
|
||||
EngramNode* n = &g->nodes[idx];
|
||||
if (getenv("EG_DIAG")) {
|
||||
fprintf(stderr, "[EG_DIAG] FORGET id=%s type=%s layer=%u label=%s\n",
|
||||
sid, n->node_type ? n->node_type : "?", n->layer_id,
|
||||
n->label ? n->label : "?");
|
||||
fflush(stderr);
|
||||
}
|
||||
free(n->id); free(n->content); free(n->node_type); free(n->label);
|
||||
free(n->tier); free(n->tags); free(n->metadata);
|
||||
free(n->emb);
|
||||
@@ -7270,6 +7276,8 @@ el_val_t engram_prune_telemetry(el_val_t older_than_ms) {
|
||||
}
|
||||
}
|
||||
g->node_count = w;
|
||||
if (getenv("EG_DIAG"))
|
||||
fprintf(stderr, "[EG_DIAG] PRUNE_TELEMETRY removed=%lld\n", (long long)removed);
|
||||
if (removed == 0) { free(removed_ids); return 0; }
|
||||
|
||||
/* Removed-id hash set (open addressing, power-of-two >= 2*removed). */
|
||||
@@ -9304,6 +9312,26 @@ el_val_t engram_load(el_val_t path) {
|
||||
}
|
||||
}
|
||||
g->adj_dirty = 1;
|
||||
if (getenv("EG_DIAG")) {
|
||||
int64_t we = 0, wrongdim = 0;
|
||||
for (int64_t i = 0; i < g->node_count; i++) {
|
||||
if (g->nodes[i].emb) { we++; if (g->nodes[i].emb_dim != 768) wrongdim++; }
|
||||
}
|
||||
fprintf(stderr, "[EG_DIAG] loaded nodes=%lld with_emb=%lld wrongdim=%lld\n",
|
||||
(long long)g->node_count, (long long)we, (long long)wrongdim);
|
||||
const char* probe = getenv("EG_DIAG_ID");
|
||||
if (probe) {
|
||||
for (int64_t i = 0; i < g->node_count; i++) {
|
||||
if (g->nodes[i].id && strcmp(g->nodes[i].id, probe) == 0) {
|
||||
fprintf(stderr, "[EG_DIAG] probe id=%s idx=%lld emb=%p dim=%d layer=%u addr=%d\n",
|
||||
probe, (long long)i, (void*)g->nodes[i].emb,
|
||||
(int)g->nodes[i].emb_dim, g->nodes[i].layer_id,
|
||||
eg_node_addressable(&g->nodes[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
fflush(stderr);
|
||||
}
|
||||
/* Walk edges array */
|
||||
const char* edges_p = json_find_key(data, "edges");
|
||||
if (edges_p) {
|
||||
@@ -9624,7 +9652,33 @@ el_val_t engram_get_node_by_label(el_val_t label) {
|
||||
return el_wrap_str(el_strdup("{}"));
|
||||
}
|
||||
|
||||
el_val_t engram_search_json(el_val_t query, el_val_t limit) {
|
||||
/* ── THE SEARCH / RECALL BOUNDARY (2026-08-07) ───────────────────────────────
|
||||
* engram_search_json is the LEXICAL function ~40 .el call sites already
|
||||
* depend on: they pass a key-shaped string ("soul:boot_count",
|
||||
* "soul-inbox-pending", a session label) and treat every returned record as
|
||||
* a record that CONTAINS that key. Seven of those sites then delete what
|
||||
* comes back (memory.el:176, sessions.el:250/268/444/523, soul.el:359 —
|
||||
* "prune all existing X nodes, keep exactly one").
|
||||
*
|
||||
* The semantic and associative legs must therefore NOT live on this
|
||||
* function. Claim 24 authorises the vector index "to respond to EMBEDDING
|
||||
* SEARCH QUERIES by returning the node records whose embedding vectors have
|
||||
* the highest cosine similarity to a query vector"; a keyed state read is
|
||||
* not an embedding search query, it is the identifier-keyed retrieval of
|
||||
* claim 23 ("node records are stored under a key encoding the node
|
||||
* identifier"). Putting both behind one function erased that boundary, and
|
||||
* a nearest neighbour of the string "soul:boot_count" is not a boot counter.
|
||||
*
|
||||
* MEASURED, on the harness corpus, isolated, read-only, no writes from any
|
||||
* caller: 240 node records destroyed per boot, including 6 Knowledge nodes,
|
||||
* a layer-1 "CORE IDENTITY — GENESIS, LINEAGE" Memory, and the value node
|
||||
* `kn-58874a74` (gold answer for gold-set q15). The deletion list is the
|
||||
* result list of the soul's own mem_boot_count_inc() lookup, in order.
|
||||
*
|
||||
* So: legs OFF here, legs ON in engram_recall_json below, which is what
|
||||
* /api/neuron/recall reaches. Retrieval quality on the recall route is
|
||||
* unchanged; the internal keyed reads get their contract back. */
|
||||
static el_val_t eg_search_json_impl(el_val_t query, el_val_t limit, int with_legs) {
|
||||
EngramStore* g = engram_get();
|
||||
const char* q = EL_CSTR(query);
|
||||
int64_t lim = (int64_t)limit;
|
||||
@@ -9646,7 +9700,7 @@ el_val_t engram_search_json(el_val_t query, el_val_t limit) {
|
||||
* so the semantic half of the retrieval surface has to land HERE
|
||||
* to be observable to the MCP wrapper and the app. */
|
||||
int32_t qdim = 0;
|
||||
float* qv = eg_embed_fetch(q, &qdim);
|
||||
float* qv = with_legs ? eg_embed_fetch(q, &qdim) : NULL;
|
||||
EngramSemEntry* sem = qv ? malloc((size_t)g->node_count * sizeof(EngramSemEntry)) : NULL;
|
||||
int64_t nsem = 0;
|
||||
int64_t nhits = 0;
|
||||
@@ -9751,6 +9805,31 @@ el_val_t engram_search_json(el_val_t query, el_val_t limit) {
|
||||
}
|
||||
qsort(hits, (size_t)nhits, sizeof(EngramRankEntry), engram_rank_w_cmp);
|
||||
if (sem) qsort(sem, (size_t)nsem, sizeof(EngramSemEntry), engram_sem_cmp);
|
||||
if (getenv("EG_DIAG")) {
|
||||
int64_t we = 0, unaddr = 0, found = 0;
|
||||
const char* pid = getenv("EG_DIAG_ID");
|
||||
for (int64_t i = 0; i < g->node_count; i++) {
|
||||
if (g->nodes[i].emb) we++;
|
||||
if (!eg_node_addressable(&g->nodes[i])) unaddr++;
|
||||
if (pid && g->nodes[i].id && strcmp(g->nodes[i].id, pid) == 0) found++;
|
||||
}
|
||||
fprintf(stderr, "[EG_DIAG] STORE node_count=%lld with_emb=%lld unaddressable=%lld probe_found=%lld\n",
|
||||
(long long)g->node_count, (long long)we, (long long)unaddr, (long long)found);
|
||||
fprintf(stderr, "[EG_DIAG] q=\"%s\" qdim=%d nhits=%lld nsem=%lld\n",
|
||||
q, (int)qdim, (long long)nhits, (long long)nsem);
|
||||
for (int64_t k = 0; k < 5 && k < nsem; k++)
|
||||
fprintf(stderr, "[EG_DIAG] sem[%lld] cos=%.4f id=%s\n",
|
||||
(long long)k, sem[k].sem, g->nodes[sem[k].idx].id);
|
||||
const char* probe = getenv("EG_DIAG_ID");
|
||||
if (probe) for (int64_t k = 0; k < nsem; k++)
|
||||
if (g->nodes[sem[k].idx].id
|
||||
&& strcmp(g->nodes[sem[k].idx].id, probe) == 0) {
|
||||
fprintf(stderr, "[EG_DIAG] probe at sem rank %lld cos=%.4f\n",
|
||||
(long long)k, sem[k].sem);
|
||||
break;
|
||||
}
|
||||
fflush(stderr);
|
||||
}
|
||||
/* Claim-10 associative leg: expand the top lexical hits along
|
||||
* structural relations only, order the reached set by query
|
||||
* similarity. Empty whenever the seeds have no structural
|
||||
@@ -9795,6 +9874,19 @@ el_val_t engram_search_json(el_val_t query, el_val_t limit) {
|
||||
return el_wrap_str(b.buf);
|
||||
}
|
||||
|
||||
/* Lexical keyed read — the historical contract every internal caller relies
|
||||
* on. Every returned record CONTAINS a query token. */
|
||||
el_val_t engram_search_json(el_val_t query, el_val_t limit) {
|
||||
return eg_search_json_impl(query, limit, 0);
|
||||
}
|
||||
|
||||
/* The retrieval surface: lexical + claim-24 semantic + claim-10 associative,
|
||||
* rank-fused. Reached from handle_api_recall (/api/neuron/recall) — the route
|
||||
* the MCP wrapper and the app call, and the one the eval harness measures. */
|
||||
el_val_t engram_recall_json(el_val_t query, el_val_t limit) {
|
||||
return eg_search_json_impl(query, limit, 1);
|
||||
}
|
||||
|
||||
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset) {
|
||||
EngramStore* g = engram_get();
|
||||
int64_t lim = (int64_t)limit; if (lim <= 0) lim = 100;
|
||||
|
||||
@@ -612,6 +612,7 @@ el_val_t engram_load(el_val_t path);
|
||||
el_val_t engram_get_node_json(el_val_t id);
|
||||
el_val_t engram_get_node_by_label(el_val_t label);
|
||||
el_val_t engram_search_json(el_val_t query, el_val_t limit);
|
||||
el_val_t engram_recall_json(el_val_t query, el_val_t limit);
|
||||
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
|
||||
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||
|
||||
Reference in New Issue
Block a user