self-review 2026-05-28: checkpoint ISE decay fix + auto-linking for MCP nodes

Three changes:

1. Fix checkpoint ISE temporal_decay_rate: engram_emit_ise_internal was
   hardcoded to 0.0 (global 168h default) instead of 2.310 (Working-tier
   48h). Result: checkpoint ISEs accumulated at 3.5x intended rate.

2. Raise CHECKPOINT_INTERVAL 1→10: checkpoint ISE fires on every single
   node write, producing 2:1 checkpoint:content ratio in ISE stream.
   MCP routes still call engram_write_binary_el explicitly after each
   important write, so no knowledge durability is lost.

3. Add auto_link_content_node to server.el: route_neuron_memory and
   route_neuron_knowledge_capture were creating nodes with zero edges —
   invisible to BFS traversal, only reachable via lexical/semantic seed.
   New helper runs BM25 over top-20 results, skips ISE nodes (which
   dominate the 14K-node corpus), connects up to 3 related nodes.
This commit is contained in:
2026-05-28 08:42:41 -05:00
parent 8f922e68b3
commit a000599bfe
4 changed files with 144 additions and 12 deletions
+80 -2
View File
@@ -227,6 +227,76 @@ fn bm25_search_json(query: String, limit: Int) -> String {
out + "]"
}
// Auto-linking
//
// auto_link_content_node link a newly-created Knowledge or Memory node to
// semantically related non-ISE nodes via BM25 search.
//
// Problem it solves: route_neuron_memory and route_neuron_knowledge_capture
// both call engram_node_full directly, creating nodes with zero edges. With
// 14K+ ISEs dominating the corpus, BFS traversal contributes nothing every
// query relies solely on lexical/semantic seed matching. Auto-linking builds
// explicit "related" edges so activated knowledge nodes fan out to connected
// neighbors during BFS.
//
// Design choices:
// - BM25 (not substring search): ranks by relevance, not just occurrence
// - Skip InternalStateEvent nodes: ISEs dominate the corpus and are not
// useful link targets for knowledge/memory nodes
// - Up to 3 edges per node: enough to build graph structure without over-linking
// - weight=0.6: moderately strong; causal edges (field-validated at 2.0) are
// much stronger, so these "related" edges don't flood activation paths
// - state_set for linked counter: EL `let` in nested if-blocks creates inner
// scope only; state_set persists across block boundaries (2026-05-25 lesson)
//
// (2026-05-28 self-review)
fn auto_link_content_node(node_id: String, content: String) -> Int {
let clen: Int = str_len(content)
if clen < 20 { return 0 }
// Find search term: first word >= 5 chars, or second word.
let sp1: Int = str_index_of(content, " ")
let w1end: Int = if sp1 < 0 { clen } else { sp1 }
let word1: String = str_slice(content, 0, w1end)
state_set("aln_term", "")
if str_len(word1) >= 5 {
state_set("aln_term", word1)
}
if str_eq(state_get("aln_term"), "") {
if sp1 >= 0 {
let rest: String = str_slice(content, sp1 + 1, clen)
let sp2: Int = str_index_of(rest, " ")
let w2end: Int = if sp2 < 0 { str_len(rest) } else { sp2 }
let word2: String = str_slice(rest, 0, w2end)
if str_len(word2) >= 5 {
state_set("aln_term", word2)
}
}
}
let search_term: String = state_get("aln_term")
if str_eq(search_term, "") { return 0 }
// BM25 over top-20 results; skip ISE nodes; connect up to 3.
let results: String = bm25_search_json(search_term, 20)
let n: Int = json_array_len(results)
state_set("aln_linked", "0")
let i: Int = 0
while i < n {
let linked_so_far: Int = str_to_int(state_get("aln_linked"))
if linked_so_far < 3 {
let elem: String = json_array_get(results, i)
let rid: String = json_get_string(elem, "id")
let rtype: String = json_get_string(elem, "node_type")
if !str_eq(rtype, "InternalStateEvent") && !str_eq(rid, "") && !str_eq(rid, node_id) {
engram_connect(node_id, rid, 0.6, "related")
state_set("aln_linked", int_to_str(linked_so_far + 1))
}
}
let i = i + 1
}
return str_to_int(state_get("aln_linked"))
}
// Helpers
fn parse_port(bind: String) -> Int {
@@ -565,13 +635,18 @@ fn route_neuron_memory(method: String, path: String, body: String) -> String {
let id: String = engram_node_full(content, node_type, label, 0.5, 0.5, 1.0, tier, tags_str)
// Auto-link to related non-ISE nodes so this memory is reachable via BFS traversal.
// Without this, MCP-created nodes arrive with zero edges and are invisible to
// graph spread during activation (only lexical/semantic seed matching finds them).
let auto_linked: Int = auto_link_content_node(id, content)
// Checkpoint after write
let dir: String = env("ENGRAM_DATA_DIR")
if str_eq(dir, "") { let dir = "/tmp/engram" }
let db_path: String = dir + "/engram.db"
engram_write_binary_el(db_path)
"{\"ok\":true,\"id\":\"" + id + "\",\"content\":\"" + str_replace(str_replace(content, "\\", "\\\\"), "\"", "\\\"") + "\"}"
"{\"ok\":true,\"id\":\"" + id + "\",\"auto_linked\":" + int_to_str(auto_linked) + ",\"content\":\"" + str_replace(str_replace(content, "\\", "\\\\"), "\"", "\\\"") + "\"}"
}
// route_neuron_knowledge_capture create a Knowledge node
@@ -611,13 +686,16 @@ fn route_neuron_knowledge_capture(method: String, path: String, body: String) ->
let id: String = engram_node_full(content, "Knowledge", title, 0.7, 0.7, 1.0, tier, tags_str)
// Auto-link to related non-ISE nodes for BFS reachability (same rationale as route_neuron_memory).
let auto_linked: Int = auto_link_content_node(id, content)
// Checkpoint
let dir: String = env("ENGRAM_DATA_DIR")
if str_eq(dir, "") { let dir = "/tmp/engram" }
let db_path: String = dir + "/engram.db"
engram_write_binary_el(db_path)
"{\"ok\":true,\"id\":\"" + id + "\"}"
"{\"ok\":true,\"id\":\"" + id + "\",\"auto_linked\":" + int_to_str(auto_linked) + "}"
}
// route_neuron_knowledge_evolve create updated node (evolution via new node)