Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bca7d8ac99 | |||
| 18714e6142 | |||
| 4936099c39 | |||
| f1471763f5 | |||
| 5850793b67 | |||
| fc1745c652 |
+134
-10
@@ -430,7 +430,130 @@ fn handle_api_node_update(body: String) -> String {
|
||||
return "{\"id\":\"" + new_id + "\",\"supersedes\":\"" + id + "\",\"ok\":true}"
|
||||
}
|
||||
|
||||
// handle_api_recall — search or activate memory by query.
|
||||
// ── Recall through spreading activation ───────────────────────────────────────
|
||||
//
|
||||
// api_activation_depth — traversal depth for a retrieval query. Honours ?depth= /
|
||||
// body "depth" for callers that want a wider or tighter associative horizon;
|
||||
// defaults to 2, matching every other production activation caller (the
|
||||
// knowledge-search path here, chat.el's per-turn activation) — one hop reaches a
|
||||
// node's direct associations, two reaches its siblings through a shared hub,
|
||||
// which is exactly the sibling-recovery case recall was failing.
|
||||
fn api_activation_depth(path: String, body: String) -> Int {
|
||||
let d: Int = api_query_int(path, "depth", 0)
|
||||
let d = if d == 0 { json_get_int(body, "depth") } else { d }
|
||||
if d <= 0 { return 2 }
|
||||
return d
|
||||
}
|
||||
|
||||
// api_merge_activated_nodes — project an activation result array down to a bare
|
||||
// node array in activation order, then backfill from the lexical seed list until
|
||||
// `limit` nodes are collected. Deduped by node id.
|
||||
//
|
||||
// SHAPE CONTRACT: the return value is a BARE array of full engram node objects —
|
||||
// byte-for-byte the same node JSON engram_search_json emits, so every existing
|
||||
// /recall consumer keeps working unchanged (the MCP wrapper's recall/
|
||||
// searchKnowledge, tools/telegram-gateway.sh which reads `.value.content`,
|
||||
// cli/neuron_mcp.py). Activation strength is a RANKING input here, not a payload
|
||||
// change; the scalars stay available on /api/activate and in compileCtx.
|
||||
fn api_merge_activated_nodes(act_raw: String, lex_raw: String, limit: Int) -> String {
|
||||
let seen: String = ""
|
||||
let out: String = ""
|
||||
let n: Int = 0
|
||||
// Pass 1 — activation-ranked. engram_activate_json already sorts promoted
|
||||
// (working-memory) nodes first by wm_weight desc, then background-only nodes
|
||||
// by background_activation desc, so element order IS the activation ranking.
|
||||
let an: Int = if api_nonempty(act_raw) { json_array_len(act_raw) } else { 0 }
|
||||
let i: Int = 0
|
||||
while i < an && n < limit {
|
||||
let entry: String = json_array_get(act_raw, i)
|
||||
let anode: String = json_get_raw(entry, "node")
|
||||
let aid: String = json_get(anode, "id")
|
||||
let adup: Bool = str_eq(aid, "") || str_contains(seen, "<" + aid + ">")
|
||||
let asep: String = if n == 0 { "" } else { "," }
|
||||
let out = if adup { out } else { out + asep + anode }
|
||||
let seen = if adup { seen } else { seen + "<" + aid + ">" }
|
||||
let n = if adup { n } else { n + 1 }
|
||||
let i = i + 1
|
||||
}
|
||||
// Pass 2 — lexical seed backfill (see the exact-lookup note on
|
||||
// handle_api_recall). Only runs when activation left room under `limit`.
|
||||
let ln: Int = if api_nonempty(lex_raw) { json_array_len(lex_raw) } else { 0 }
|
||||
let j: Int = 0
|
||||
while j < ln && n < limit {
|
||||
let lnode: String = json_array_get(lex_raw, j)
|
||||
let lid: String = json_get(lnode, "id")
|
||||
let ldup: Bool = str_eq(lid, "") || str_contains(seen, "<" + lid + ">")
|
||||
let lsep: String = if n == 0 { "" } else { "," }
|
||||
let out = if ldup { out } else { out + lsep + lnode }
|
||||
let seen = if ldup { seen } else { seen + "<" + lid + ">" }
|
||||
let n = if ldup { n } else { n + 1 }
|
||||
let j = j + 1
|
||||
}
|
||||
return "[" + out + "]"
|
||||
}
|
||||
|
||||
// api_retrieve — THE retrieval path. Spreading activation over the weighted
|
||||
// directed graph, lexical seeds backfilling the tail.
|
||||
//
|
||||
// WAS (until 2026-08-07): `engram_search_json(q, limit)` alone — a case-
|
||||
// insensitive substring matcher scored by how many distinct query tokens appear
|
||||
// in a node's content/label/tags, tie-broken by raw salience. It never read a
|
||||
// single edge. Recall could not see an association: querying an identity value
|
||||
// returned unrelated documents that happened to contain the word, and NOT the
|
||||
// twelve sibling value nodes one hop off the same hub.
|
||||
//
|
||||
// NOW: recall runs the spreading-activation traversal that has been compiled
|
||||
// into the runtime the whole time (engram_activate / engram_activate_json,
|
||||
// el_runtime.c) and ranks by the resulting activation strength. This restores
|
||||
// the designed retrieval mechanism — Engram provisional 64/064,260, claim 1:
|
||||
// "no data is retrieved from the weighted directed graph except through the
|
||||
// spreading activation traversal", with activation strength computed as the
|
||||
// PRODUCT of parent strength, edge weight, target salience, and query/target
|
||||
// cosine similarity, because "the multiplication of all four factors enforces a
|
||||
// conjunctive property... addition would allow many weak associations to
|
||||
// accumulate into false relevance."
|
||||
//
|
||||
// SEEDING — derived from the runtime, not assumed. engram_activate takes the
|
||||
// query TEXT (not seed ids) and seeds internally in two passes: (1) lexical —
|
||||
// every node matching at least one query token seeds, with initial activation
|
||||
// = salience x temporal_decay x dampening x token_coverage, so a node covering
|
||||
// the whole phrase ignites harder than one covering a single word; (2) semantic
|
||||
// supplement — the top-K unreached nodes by cosine against the query embedding.
|
||||
// All four other production call sites (neuron-api.el begin_session/compileCtx,
|
||||
// chat.el:352/1715, awareness.el's curiosity scans) pass query text the same
|
||||
// way, so this follows the established convention exactly. The consequence for
|
||||
// recall is direct: the lexical surface recall used to RETURN is now the SEED
|
||||
// SET of the traversal, and what comes back is what those seeds activate. That
|
||||
// is why multi-word queries stop returning nothing — every token that matches
|
||||
// anything ignites, and the traversal ranks the resulting field.
|
||||
//
|
||||
// EXACT-LOOKUP GUARANTEE (no regression): engram_activate's result collector
|
||||
// drops any reached node whose background_activation x confidence < 0.1 unless
|
||||
// it was promoted to working memory, and it never seeds from InternalStateEvent
|
||||
// nodes. So a rare exact token on a dormant, low-salience node can seed the
|
||||
// traversal and still go unreported. Retrieval therefore appends the lexical
|
||||
// seed list after the activated ranking, deduped by id, until `limit` is filled.
|
||||
// This is a seeded hybrid, not a parallel search bolted alongside activation:
|
||||
// the backfill is the SAME seed set the traversal itself computed, restored to
|
||||
// the tail of the result rather than recomputed by a different mechanism.
|
||||
// Activation always leads the ranking; nothing that used to be findable becomes
|
||||
// unfindable.
|
||||
//
|
||||
// COST/EFFECT NOTE: activation is a stateful read by design — claim 29, "update
|
||||
// the last-activation timestamp and increment the activation count... in
|
||||
// response to any access to that node record during spreading activation
|
||||
// traversal". Promoted nodes get reinforced, working-memory weights are
|
||||
// rewritten, and the query folds into the context centroid. That is the
|
||||
// intended semantics of retrieval-as-activation and is already what every chat
|
||||
// turn does; it does mean recall now participates in shaping working memory.
|
||||
fn api_retrieve(q: String, path: String, body: String, limit: Int) -> String {
|
||||
let depth: Int = api_activation_depth(path, body)
|
||||
let act_raw: String = engram_activate_json(q, depth)
|
||||
let lex_raw: String = engram_search_json(q, limit)
|
||||
return api_or_empty(api_merge_activated_nodes(act_raw, lex_raw, limit))
|
||||
}
|
||||
|
||||
// handle_api_recall — retrieve memory by query, through spreading activation.
|
||||
fn handle_api_recall(method: String, path: String, body: String) -> String {
|
||||
// Accept the query from the URL ?query= / ?q= params, or, when those are
|
||||
// empty (e.g. a POST with a JSON body), from the body fields "query"/"q".
|
||||
@@ -450,8 +573,7 @@ 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)
|
||||
return api_or_empty(results)
|
||||
return api_retrieve(eff_q, path, body, limit)
|
||||
}
|
||||
|
||||
// ── Knowledge ─────────────────────────────────────────────────────────────────
|
||||
@@ -470,13 +592,15 @@ fn handle_api_search_knowledge(method: String, path: String, body: String) -> St
|
||||
let limit = if limit == 0 { json_get_int(body, "limit") } else { limit }
|
||||
let limit = if limit == 0 { 10 } else { limit }
|
||||
if str_eq(q, "") { return api_err("query is required") }
|
||||
let results: String = engram_search_json(q, limit)
|
||||
if str_eq(results, "") { return "[]" }
|
||||
let first: String = str_slice(results, 0, 1)
|
||||
if !str_eq(first, "[") && !str_eq(first, "{") {
|
||||
return api_or_empty(engram_activate_json(q, 2))
|
||||
}
|
||||
return results
|
||||
// Same retrieval path as recall — and it is the SAME change, not a copy of
|
||||
// one. The "activate fallback" this replaced was unreachable dead code: it
|
||||
// only fired when engram_search_json's return did not start with '[' or '{',
|
||||
// and engram_search_json always emits a '['-prefixed array (el_runtime.c
|
||||
// jb_putc('[') before any hit test), so the guard was false on every call
|
||||
// including the zero-hit "[]" case. Knowledge search therefore had exactly
|
||||
// the substring-matcher behavior recall had, with a comment claiming
|
||||
// otherwise. Routing it through api_retrieve makes the claim true.
|
||||
return api_retrieve(q, path, body, limit)
|
||||
}
|
||||
|
||||
// handle_api_browse_knowledge — list Knowledge nodes.
|
||||
|
||||
Reference in New Issue
Block a user