self-review 2026-05-16: tier-based decay rates, implement knowledge_promote, ISE label extraction
Three research-grounded improvements: 1. Tier-based temporal decay in el_runtime.c (engram_node_full, engram_node_layered): Working=48h, Episodic=72h, Semantic=336h, Procedural=720h half-lives. Grounded in ACT-R literature — differentiated decay by chunk type. The temporal_decay_rate field existed but was always 0 (global 168h for everything). New nodes now carry the correct half-life for their tier from creation. 2. Implement route_neuron_knowledge_promote in server.el (was a silent stub): Reads existing node, creates promoted-tier copy with supersedes edge, checkpoints. promote_knowledge MCP tool now has real effect. 3. ISE label extraction + offset support in route_neuron_state_events: POST now extracts 'event' field from content JSON as label (heartbeat, wm_promotion, etc.) instead of always writing 'state-event'. GET now accepts ?offset= for pagination to reach recent ISEs.
This commit is contained in:
Vendored
BIN
Binary file not shown.
Vendored
+62
-3
@@ -685,7 +685,62 @@ el_val_t route_neuron_knowledge_evolve(el_val_t method, el_val_t path, el_val_t
|
||||
}
|
||||
|
||||
el_val_t route_neuron_knowledge_promote(el_val_t method, el_val_t path, el_val_t body) {
|
||||
return EL_STR("{\"ok\":true}");
|
||||
el_val_t id = json_get_string(body, EL_STR("id"));
|
||||
if (str_eq(id, EL_STR(""))) {
|
||||
return EL_STR("{\"ok\":true}");
|
||||
}
|
||||
el_val_t node_json = engram_get_node_json(id);
|
||||
if (str_eq(node_json, EL_STR(""))) {
|
||||
return err_json(EL_STR("node not found"));
|
||||
}
|
||||
if (str_eq(node_json, EL_STR("null"))) {
|
||||
return err_json(EL_STR("node not found"));
|
||||
}
|
||||
el_val_t content = json_get_string(node_json, EL_STR("content"));
|
||||
if (str_eq(content, EL_STR(""))) {
|
||||
return err_json(EL_STR("node has no content"));
|
||||
}
|
||||
el_val_t label = json_get_string(node_json, EL_STR("label"));
|
||||
el_val_t tags = json_get_string(node_json, EL_STR("tags"));
|
||||
el_val_t current_tier = json_get_string(node_json, EL_STR("tier"));
|
||||
el_val_t tier_raw = json_get_string(body, EL_STR("tier"));
|
||||
el_val_t new_tier = EL_STR("");
|
||||
if (str_eq(tier_raw, EL_STR("lesson"))) {
|
||||
new_tier = EL_STR("Semantic");
|
||||
}
|
||||
if (str_eq(tier_raw, EL_STR("canonical"))) {
|
||||
new_tier = EL_STR("Procedural");
|
||||
}
|
||||
if (str_eq(tier_raw, EL_STR("note"))) {
|
||||
new_tier = EL_STR("Episodic");
|
||||
}
|
||||
if (str_eq(new_tier, EL_STR(""))) {
|
||||
if (str_eq(current_tier, EL_STR("Working"))) {
|
||||
new_tier = EL_STR("Episodic");
|
||||
}
|
||||
if (str_eq(current_tier, EL_STR("Episodic"))) {
|
||||
new_tier = EL_STR("Semantic");
|
||||
}
|
||||
if (str_eq(current_tier, EL_STR("Semantic"))) {
|
||||
new_tier = EL_STR("Procedural");
|
||||
}
|
||||
if (str_eq(current_tier, EL_STR("Procedural"))) {
|
||||
new_tier = EL_STR("Procedural");
|
||||
}
|
||||
}
|
||||
if (str_eq(new_tier, EL_STR(""))) {
|
||||
new_tier = EL_STR("Semantic");
|
||||
}
|
||||
el_val_t new_id = engram_node_full(content, EL_STR("Knowledge"), label, el_from_float(0.7), el_from_float(0.8), el_from_float(1.0), new_tier, tags);
|
||||
if (!str_eq(new_id, EL_STR(""))) {
|
||||
engram_connect(new_id, id, el_from_float(1.0), EL_STR("supersedes"));
|
||||
}
|
||||
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
|
||||
if (str_eq(dir, EL_STR(""))) {
|
||||
dir = EL_STR("/tmp/engram");
|
||||
}
|
||||
engram_write_binary_el(el_str_concat(dir, EL_STR("/engram.db")));
|
||||
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), new_id), EL_STR("\",\"promoted_from\":\"")), id), EL_STR("\",\"tier\":\"")), new_tier), EL_STR("\"}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -768,13 +823,17 @@ el_val_t route_neuron_state_events(el_val_t method, el_val_t path, el_val_t body
|
||||
if (str_eq(method, EL_STR("GET"))) {
|
||||
el_val_t limit_str = query_param(path, EL_STR("limit"));
|
||||
el_val_t limit = ({ el_val_t _if_result_6 = 0; if (str_eq(limit_str, EL_STR(""))) { _if_result_6 = (50); } else { _if_result_6 = (str_to_int(limit_str)); } _if_result_6; });
|
||||
return engram_scan_nodes_by_type_json(EL_STR("InternalStateEvent"), limit, 0);
|
||||
el_val_t offset_str = query_param(path, EL_STR("offset"));
|
||||
el_val_t offset = ({ el_val_t _if_result_7 = 0; if (str_eq(offset_str, EL_STR(""))) { _if_result_7 = (0); } else { _if_result_7 = (str_to_int(offset_str)); } _if_result_7; });
|
||||
return engram_scan_nodes_by_type_json(EL_STR("InternalStateEvent"), limit, offset);
|
||||
}
|
||||
el_val_t content = json_get_string(body, EL_STR("content"));
|
||||
if (str_eq(content, EL_STR(""))) {
|
||||
content = body;
|
||||
}
|
||||
el_val_t id = engram_node_full(content, EL_STR("InternalStateEvent"), EL_STR("state-event"), el_from_float(0.3), el_from_float(0.3), el_from_float(1.0), EL_STR("Working"), EL_STR("internal-state"));
|
||||
el_val_t event_label = json_get_string(content, EL_STR("event"));
|
||||
el_val_t label = ({ el_val_t _if_result_8 = 0; if (str_eq(event_label, EL_STR(""))) { _if_result_8 = (EL_STR("state-event")); } else { _if_result_8 = (event_label); } _if_result_8; });
|
||||
el_val_t id = engram_node_full(content, EL_STR("InternalStateEvent"), label, el_from_float(0.3), el_from_float(0.3), el_from_float(1.0), EL_STR("Working"), EL_STR("internal-state"));
|
||||
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), id), EL_STR("\"}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
+62
-5
@@ -629,9 +629,57 @@ fn route_neuron_knowledge_evolve(method: String, path: String, body: String) ->
|
||||
"{\"ok\":true,\"id\":\"" + id + "\"}"
|
||||
}
|
||||
|
||||
// route_neuron_knowledge_promote — stub: ok
|
||||
// route_neuron_knowledge_promote — promote a knowledge node to a higher tier.
|
||||
// Creates a new node with the promoted tier (same content) and connects
|
||||
// via a "supersedes" edge from new → old. Tier mapping:
|
||||
// note/Episodic → lesson/Semantic → canonical/Procedural
|
||||
fn route_neuron_knowledge_promote(method: String, path: String, body: String) -> String {
|
||||
"{\"ok\":true}"
|
||||
let id: String = json_get_string(body, "id")
|
||||
if str_eq(id, "") { return "{\"ok\":true}" }
|
||||
|
||||
// Read existing node
|
||||
let node_json: String = engram_get_node_json(id)
|
||||
if str_eq(node_json, "") { return err_json("node not found") }
|
||||
if str_eq(node_json, "null") { return err_json("node not found") }
|
||||
|
||||
let content: String = json_get_string(node_json, "content")
|
||||
if str_eq(content, "") { return err_json("node has no content") }
|
||||
let label: String = json_get_string(node_json, "label")
|
||||
let tags: String = json_get_string(node_json, "tags")
|
||||
let current_tier: String = json_get_string(node_json, "tier")
|
||||
|
||||
// Determine target tier: explicit override or auto-promote one level
|
||||
let tier_raw: String = json_get_string(body, "tier")
|
||||
let new_tier: String = ""
|
||||
|
||||
// Explicit tier takes precedence
|
||||
if str_eq(tier_raw, "lesson") { let new_tier = "Semantic" }
|
||||
if str_eq(tier_raw, "canonical") { let new_tier = "Procedural" }
|
||||
if str_eq(tier_raw, "note") { let new_tier = "Episodic" }
|
||||
|
||||
// Auto-promote one level if no explicit tier
|
||||
if str_eq(new_tier, "") {
|
||||
if str_eq(current_tier, "Working") { let new_tier = "Episodic" }
|
||||
if str_eq(current_tier, "Episodic") { let new_tier = "Semantic" }
|
||||
if str_eq(current_tier, "Semantic") { let new_tier = "Procedural" }
|
||||
if str_eq(current_tier, "Procedural") { let new_tier = "Procedural" }
|
||||
}
|
||||
if str_eq(new_tier, "") { let new_tier = "Semantic" }
|
||||
|
||||
// Create promoted node — higher importance (0.8) signals durable knowledge
|
||||
let new_id: String = engram_node_full(content, "Knowledge", label, 0.7, 0.8, 1.0, new_tier, tags)
|
||||
|
||||
// Wire supersedes edge: new node supersedes old
|
||||
if !str_eq(new_id, "") {
|
||||
engram_connect(new_id, id, 1.0, "supersedes")
|
||||
}
|
||||
|
||||
// Checkpoint
|
||||
let dir: String = env("ENGRAM_DATA_DIR")
|
||||
if str_eq(dir, "") { let dir = "/tmp/engram" }
|
||||
engram_write_binary_el(dir + "/engram.db")
|
||||
|
||||
"{\"ok\":true,\"id\":\"" + new_id + "\",\"promoted_from\":\"" + id + "\",\"tier\":\"" + new_tier + "\"}"
|
||||
}
|
||||
|
||||
// route_neuron_recall — search or list nodes
|
||||
@@ -702,16 +750,25 @@ fn route_neuron_config(method: String, path: String, body: String) -> String {
|
||||
"{\"key\":\"" + key + "\",\"value\":\"\"}"
|
||||
}
|
||||
|
||||
// route_neuron_state_events — GET lists ISEs, POST logs a new one
|
||||
// route_neuron_state_events — GET lists ISEs, POST logs a new one.
|
||||
// GET supports ?limit=N&offset=M for pagination; ?label=X to extract label
|
||||
// from the ISE content's "event" field.
|
||||
// Use a high offset to skip to recent ISEs (ISEs fill quickly and sort by
|
||||
// salience then insertion order — older entries dominate the front of scans).
|
||||
fn route_neuron_state_events(method: String, path: String, body: String) -> String {
|
||||
if str_eq(method, "GET") {
|
||||
let limit_str: String = query_param(path, "limit")
|
||||
let limit: Int = if str_eq(limit_str, "") { 50 } else { str_to_int(limit_str) }
|
||||
return engram_scan_nodes_by_type_json("InternalStateEvent", limit, 0)
|
||||
let offset_str: String = query_param(path, "offset")
|
||||
let offset: Int = if str_eq(offset_str, "") { 0 } else { str_to_int(offset_str) }
|
||||
return engram_scan_nodes_by_type_json("InternalStateEvent", limit, offset)
|
||||
}
|
||||
let content: String = json_get_string(body, "content")
|
||||
if str_eq(content, "") { let content = body }
|
||||
let id: String = engram_node_full(content, "InternalStateEvent", "state-event", 0.3, 0.3, 1.0, "Working", "internal-state")
|
||||
// Extract label from content JSON "event" field for better ISE searchability
|
||||
let event_label: String = json_get_string(content, "event")
|
||||
let label: String = if str_eq(event_label, "") { "state-event" } else { event_label }
|
||||
let id: String = engram_node_full(content, "InternalStateEvent", label, 0.3, 0.3, 1.0, "Working", "internal-state")
|
||||
"{\"ok\":true,\"id\":\"" + id + "\"}"
|
||||
}
|
||||
|
||||
|
||||
@@ -6070,7 +6070,20 @@ el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label,
|
||||
if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5;
|
||||
if (n->importance <= 0.0 || n->importance > 1.0) n->importance = 0.5;
|
||||
if (n->confidence <= 0.0 || n->confidence > 1.0) n->confidence = 1.0;
|
||||
n->temporal_decay_rate = 0.0; /* 0 = use global default ENGRAM_DECAY_LAMBDA */
|
||||
/* Tier-based temporal decay (2026-05-16 self-review, grounded in ACT-R literature).
|
||||
* Working-memory events fade fastest; semantic knowledge persists 2 weeks;
|
||||
* procedural/canonical knowledge lasts ~30 days before 50% decay.
|
||||
* Formula: lambda = ENGRAM_DECAY_LAMBDA * ENGRAM_T_HALF_HOURS / target_half_life_h
|
||||
* Working=48h → 2.310, Episodic=72h → 1.617, Semantic=336h → 0.347, Procedural=720h → 0.162 */
|
||||
if (ti && *ti) {
|
||||
if (strcmp(ti, "Working") == 0) n->temporal_decay_rate = 2.310;
|
||||
else if (strcmp(ti, "Episodic") == 0) n->temporal_decay_rate = 1.617;
|
||||
else if (strcmp(ti, "Semantic") == 0) n->temporal_decay_rate = 0.347;
|
||||
else if (strcmp(ti, "Procedural") == 0) n->temporal_decay_rate = 0.162;
|
||||
else n->temporal_decay_rate = 0.0;
|
||||
} else {
|
||||
n->temporal_decay_rate = 0.0; /* global default: 168h half-life */
|
||||
}
|
||||
n->activation_count = 0;
|
||||
int64_t now = engram_now_ms();
|
||||
n->last_activated = now;
|
||||
@@ -6146,7 +6159,8 @@ el_val_t engram_node_layered(el_val_t content, el_val_t node_type, el_val_t labe
|
||||
if (n->salience <= 0.0 || n->salience > 1.0) n->salience = 0.5;
|
||||
if (n->importance <= 0.0 || n->importance > 1.0) n->importance = 0.5;
|
||||
if (n->confidence <= 0.0 || n->confidence > 1.0) n->confidence = 1.0;
|
||||
n->temporal_decay_rate = 0.0;
|
||||
/* engram_node_layered always creates Working-tier nodes — apply 48h decay */
|
||||
n->temporal_decay_rate = 2.310;
|
||||
n->activation_count = 0;
|
||||
int64_t now = engram_now_ms();
|
||||
n->last_activated = now;
|
||||
|
||||
Reference in New Issue
Block a user