diff --git a/engram/dist/engram b/engram/dist/engram index 9910b3c..b187b41 100755 Binary files a/engram/dist/engram and b/engram/dist/engram differ diff --git a/engram/dist/engram.c b/engram/dist/engram.c index abce547..e8c3c93 100644 --- a/engram/dist/engram.c +++ b/engram/dist/engram.c @@ -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; } diff --git a/engram/src/server.el b/engram/src/server.el index d3e94d7..a3aafe3 100644 --- a/engram/src/server.el +++ b/engram/src/server.el @@ -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 + "\"}" } diff --git a/lang/releases/v1.0.0-20260501/el_runtime.c b/lang/releases/v1.0.0-20260501/el_runtime.c index a66a5e9..08ab3cd 100644 --- a/lang/releases/v1.0.0-20260501/el_runtime.c +++ b/lang/releases/v1.0.0-20260501/el_runtime.c @@ -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;