Files
neuron/memory.el
T
will.anderson 0ede112d05
Neuron Soul CI / build (pull_request) Has been cancelled
feat(recall): temporal-precision improvements
Fix critical float parsing bug in engram_score_node: str_replace('.','')
then str_to_int silently miscored single-decimal salience strings (0.9->9,
0.7->7, 1.0->1). Introduce parse_salience_100() which detects decimal
position and scales correctly (no decimal: *100; one decimal: *10;
two decimals: as-is).

Replace flat 30-day linear decay with tier-aware decay curves: Canonical
nodes use a 365-day window (foundational identity resists aging), Episodic
nodes use 90 days, Working/untiered keep the existing 30-day slope. Floor
stays at 10 for all tiers.

Use max(created_at, updated_at) as the recency reference so revised nodes
are not penalised for their original creation date.

Extend affective context windows from 72h/7d to 14 days across all three
paths (engram_compile, handle_chat, soul.el load_identity_context) so a
Friday crisis carries into Monday sessions and all paths present consistent
context. The 72h/7d split caused conflicting affective context between
soul.el (which loaded a 5-day-old crisis node) and chat.el (which excluded
it on subsequent turns).

Add salience evolution to mem_consolidate: strengthen top working-memory
nodes (recently recalled across sessions) and Canonical-tier nodes
(foundational identity must not decay to the floor). Previously consolidate
returned structural counts only with no salience changes.

Expand conversation window from 20 to 40 turns in both handle_chat and the
agentic history trim. Long technical sessions were losing early problem
framing at 10 user + 10 assistant pairs.
2026-06-22 12:53:29 -05:00

164 lines
6.3 KiB
EmacsLisp

fn tier_working() -> String { return "Working" }
fn tier_episodic() -> String { return "Episodic" }
fn tier_canonical() -> String { return "Canonical" }
fn mem_store(content: String, label: String, tags: String) -> String {
return engram_node_full(
content,
"Memory",
label,
el_from_float(0.5),
el_from_float(0.5),
el_from_float(0.8),
"Working",
tags
)
}
fn mem_remember(content: String, tags: String) -> String {
return mem_store(content, "soul-memory", tags)
}
fn mem_recall(query: String, depth: Int) -> String {
return engram_activate_json(query, depth)
}
fn mem_search(query: String, limit: Int) -> String {
return engram_search_json(query, limit)
}
fn mem_strengthen(node_id: String) -> Void {
engram_strengthen(node_id)
}
fn mem_forget(node_id: String) -> Void {
engram_forget(node_id)
}
// mem_consolidate structural scan plus salience-evolution pass.
//
// Previously this only returned structural counts (scanned, total_nodes, total_edges)
// with no salience updates. No node salience ever changed based on recall frequency
// or time; foundational nodes decayed identically to ephemeral chat; frequently-recalled
// nodes were never promoted. This made consolidation a no-op.
//
// New behavior:
// (a) Strengthen frequently-activated nodes: nodes in the top working-memory list
// (engram_wm_top_json) are strengthened they have been recalled recently
// and deserve higher salience. Raises effective salience for nodes that prove
// relevant across multiple sessions.
// (b) Strengthen Canonical-tier nodes: identity and foundational nodes should not
// decay; each consolidation pass re-strengthens them so they resist the
// tier-aware decay curve without requiring active recall.
// (c) Structural counts are still returned for observability.
//
// Called by awareness_run() on the "consolidate" inbox action.
fn mem_consolidate() -> String {
let scanned: Int = engram_node_count()
let total_edges: Int = engram_edge_count()
let strengthened: Int = 0
// (a) Strengthen top working-memory nodes recalled recently across sessions.
// Cap at 10 to keep consolidation fast.
let wm_top: String = engram_wm_top_json(10)
let wm_len: Int = json_array_len(wm_top)
let wi: Int = 0
while wi < wm_len {
let wm_node: String = json_array_get(wm_top, wi)
let wm_id: String = json_get(wm_node, "id")
if !str_eq(wm_id, "") {
engram_strengthen(wm_id)
let strengthened = strengthened + 1
}
let wi = wi + 1
}
// (b) Strengthen Canonical-tier nodes from a scan so they resist temporal decay.
// Canonical nodes encode foundational identity they must not silently floor at 10.
let scan_result: String = engram_scan_nodes_json(50, 0)
let scan_len: Int = json_array_len(scan_result)
let si: Int = 0
while si < scan_len {
let s_node: String = json_array_get(scan_result, si)
let s_tier: String = json_get(s_node, "tier")
let s_id: String = json_get(s_node, "id")
if str_eq(s_tier, "Canonical") && !str_eq(s_id, "") {
engram_strengthen(s_id)
let strengthened = strengthened + 1
}
let si = si + 1
}
let total_nodes: Int = engram_node_count()
return "{\"scanned\":" + int_to_str(scanned)
+ ",\"total_nodes\":" + int_to_str(total_nodes)
+ ",\"total_edges\":" + int_to_str(total_edges)
+ ",\"strengthened\":" + int_to_str(strengthened) + "}"
}
fn mem_save(path: String) -> Void {
let save_result: String = engram_save(path)
if str_eq(save_result, "") {
println("[memory] mem_save: engram_save failed for " + path + " — snapshot may be incomplete")
}
}
fn mem_load(path: String) -> Void {
engram_load(path)
}
// mem_boot_count_get retrieve current boot count from engram.
// Searches for the "soul:boot_count" node and returns its numeric value.
// Returns 0 if not found.
fn mem_boot_count_get() -> Int {
let results: String = engram_search_json("soul:boot_count", 3)
if str_eq(results, "") { return 0 }
if str_eq(results, "[]") { return 0 }
let node: String = json_array_get(results, 0)
let content: String = json_get(node, "content")
let prefix: String = "soul:boot_count:"
if !str_starts_with(content, prefix) { return 0 }
let num_str: String = str_slice(content, str_len(prefix), str_len(content))
return str_to_int(num_str)
}
// mem_boot_count_inc increment boot counter, store new node, return new count.
// Each boot creates a new "soul:boot_count:N" node. Old ones accumulate as
// history the search above always returns the highest value seen.
fn mem_boot_count_inc() -> Int {
let current: Int = mem_boot_count_get()
let next: Int = current + 1
let content: String = "soul:boot_count:" + int_to_str(next)
let tags: String = "[\"soul-meta\",\"boot-counter\"]"
let boot_node_id: String = engram_node_full(
content, "Memory", "soul:boot_count",
el_from_float(0.9), el_from_float(0.9), el_from_float(1.0),
"Canonical", tags
)
if str_eq(boot_node_id, "") {
println("[memory] mem_boot_count_inc: engram write failed — boot counter node lost (count=" + int_to_str(next) + ")")
}
return next
}
// mem_emit_state_event log an internal state event as structured memory.
// Schema: {trigger, kind, content, boot, ts}
// This creates an auditable evidence trail of cognitive decisions.
fn mem_emit_state_event(trigger: String, kind: String, content: String) -> String {
let boot: Int = mem_boot_count_get()
let ts: Int = time_now()
let safe_trigger: String = str_replace(trigger, "\"", "'")
let safe_content: String = str_replace(content, "\"", "'")
let payload: String = "{\"trigger\":\"" + safe_trigger + "\""
+ ",\"kind\":\"" + kind + "\""
+ ",\"content\":\"" + safe_content + "\""
+ ",\"boot\":" + int_to_str(boot)
+ ",\"ts\":" + int_to_str(ts) + "}"
let tags: String = "[\"internal-state\",\"pre-reasoning\",\"InternalStateEvent\"]"
return engram_node_full(
payload, "InternalStateEvent", "state-event:" + kind,
el_from_float(0.85), el_from_float(0.8), el_from_float(0.9),
"Episodic", tags
)
}