Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b9ef66cae9 |
+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