Files
neuron/memory.el
will.anderson fb0bb553f3 self-review 2026-07-24: boot counter — demote to telemetry weight, restore persistence via HTTP write-back
Three stale soul:boot_count copies (salience .9, importance .9, Canonical:
+0.2 tier bias, 0.15 threshold) held the top WM slots for 23h — a boot
counter outcompeting real context. Demoted to salience .55 / importance .2 /
tier Working: plumbing, not memory.

Persistence was also broken: in HTTP-engram mode the server owns state and
nothing wrote the counter back — the log shows boot #5 on three consecutive
boots. mem_boot_count_inc now mirrors the persona write-back: delete stale
server copies (matched by content prefix — route_create_node sets
label=content), create the replacement server-side. Working tier is in the
boot seed (/api/nodes) but excluded from periodic /api/sync, so the count
survives restarts without re-importing mid-session. Verified: restart
incremented 1->2 with exactly one server-side counter node.
2026-07-24 08:53:02 -05:00

281 lines
13 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 {
let id: String = engram_node_full(
content,
"Memory",
label,
el_from_float(0.5),
el_from_float(0.5),
el_from_float(0.8),
"Working",
tags
)
if str_eq(id, "") {
println("[memory] write rejected by engram (empty id): label=" + label)
return ""
}
// Read back to verify the node actually persisted guards against silent write failures.
let readback: String = engram_get_node_json(id)
if str_eq(readback, "") || str_eq(readback, "{}") {
println("[memory] WRITE VERIFY FAILED: label=" + label + " id=" + id + " — node absent after write")
return ""
}
println("[memory] write verified: " + id + " ok")
return id
}
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)
}
// mem_tombstone immutable "delete": KEEP the node and all its edges; record a
// Tombstone marker (content = target id, label "tombstone:<id>", wired with a
// "tombstones" edge). Never engram_forget. Default bounded list reads hide
// tombstoned nodes; ?include_deleted=1 recovers them. This is the ONE canonical
// tombstone helper every forget path routes through it. Defined here in
// memory.el (imported first) so awareness.el and neuron-api.el can both call it.
fn mem_tombstone(node_id: String) -> String {
let tags: String = "[\"Tombstone\",\"status:deleted\"]"
let marker: String = engram_node_full(
node_id, "Tombstone", "tombstone:" + node_id,
el_from_float(0.01), el_from_float(0.01), el_from_float(1.0),
"Episodic", tags)
if !str_eq(marker, "") {
engram_connect(marker, node_id, el_from_float(1.0), "tombstones")
}
return marker
}
// mem_forget NOTE: no longer a hard delete. Engram nodes are immutable, so
// this now TOMBSTONES (via mem_tombstone): the node and its edges are kept and
// stay recoverable. Every caller (the /memory/forget route and the cultivate
// forget op) is non-destructive as a result. Internal GC that genuinely needs
// removal (session-summary replace, telemetry pruning) calls engram_forget
// directly and is unaffected by this.
fn mem_forget(node_id: String) -> Void {
let _marker: String = mem_tombstone(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 {
// engram_save returns an Int (1 = ok, 0 = failure), NOT a String. Calling
// str_eq on it casts EL_CSTR(1) -> (char*)0x1 and SIGSEGVs on a SUCCESSFUL
// save which is exactly what a fresh-install genesis boot does first
// (seeds the brain, saves, crashes). This is issue #150. Check the Int.
let saved: Int = engram_save(path)
if saved == 0 {
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 a single canonical node, return new count.
// Prunes ALL existing soul:boot_count nodes before inserting the new one so there is
// always at most ONE such node in the graph. Without pruning, engram_node_full inserts
// a new node every boot (no upsert) and the old ones accumulate. The search-first
// approach also fixes a latent ordering bug: engram_search_json returns oldest-first,
// so mem_boot_count_get() with limit=3 would read a stale (lower) count once more
// than 3 copies accumulate.
fn mem_boot_count_inc() -> Int {
let current: Int = mem_boot_count_get()
let next: Int = current + 1
// Prune all existing boot_count nodes keep exactly one.
let old_results: String = engram_search_json("soul:boot_count", 50)
if !str_eq(old_results, "") && !str_eq(old_results, "[]") {
let old_len: Int = json_array_len(old_results)
let oi: Int = 0
while oi < old_len {
let old_node: String = json_array_get(old_results, oi)
let old_id: String = json_get(old_node, "id")
if !str_eq(old_id, "") {
engram_forget(old_id)
}
let oi = oi + 1
}
}
let content: String = "soul:boot_count:" + int_to_str(next)
let tags: String = "[\"soul-meta\",\"boot-counter\"]"
// TELEMETRY DEMOTION (2026-07-24 self-review): this counter was written at
// salience 0.9 / importance 0.9 / tier Canonical Canonical gets +0.2
// goal bias and the 0.15 promotion threshold, so three stale copies of a
// BOOT COUNTER held the top working-memory slots (wm 0.31+) for 23h,
// crowding out real context. It is plumbing, not memory. Working tier:
// 0.40 threshold, no tier bias, and the /api/sync route excludes
// Working-tier nodes so the counter also stops leaking to the engram
// server graph, which is where the duplicate copies accumulated (the
// prune below only reaches the soul's local graph). Persistence across
// restarts comes from the soul's own snapshot (mem_save), not from sync.
let boot_node_id: String = engram_node_full(
content, "Memory", "soul:boot_count",
el_from_float(0.55), el_from_float(0.2), el_from_float(1.0),
"Working", tags
)
if str_eq(boot_node_id, "") {
println("[memory] mem_boot_count_inc: write rejected (empty id) — boot counter node lost (count=" + int_to_str(next) + ")")
return next
}
let boot_readback: String = engram_get_node_json(boot_node_id)
if str_eq(boot_readback, "") || str_eq(boot_readback, "{}") {
println("[memory] mem_boot_count_inc: WRITE VERIFY FAILED id=" + boot_node_id + " count=" + int_to_str(next))
}
// HTTP WRITE-BACK (2026-07-24 self-review): in HTTP-engram mode the server
// owns persistence and the soul's in-process graph dies with the process
// the local create above is invisible to the next boot. The counter only
// ever "persisted" via a long-gone push path, which is why the log shows
// boot #5 on three consecutive boots. Mirror the persona write-back
// pattern: delete stale server copies (dupes were the 23h WM-pollution
// bug), then create the demoted replacement server-side. Boot seeding
// reads /api/nodes, which includes Working-tier nodes, so the count
// survives restarts; the periodic /api/sync excludes Working tier, so it
// never re-imports mid-session. Fail-soft: on any HTTP error the counter
// is in-memory-only this session, same as before.
let wb_url: String = env("ENGRAM_URL")
let wb_key: String = env("ENGRAM_API_KEY")
if !str_eq(wb_url, "") && !str_eq(wb_key, "") {
let auth_body: String = "{\"_auth\":\"" + json_safe(wb_key) + "\"}"
let srv_old: String = http_get(wb_url + "/api/search?q=soul:boot_count&limit=20")
if !str_eq(srv_old, "") && !str_eq(srv_old, "[]") {
let srv_len: Int = json_array_len(srv_old)
let si: Int = 0
while si < srv_len {
let srv_node: String = json_array_get(srv_old, si)
// Match by CONTENT prefix, not label: route_create_node sets
// label = content, so server-side counter nodes carry labels
// like "soul:boot_count:6". (2026-07-24)
let srv_content: String = json_get(srv_node, "content")
if str_starts_with(srv_content, "soul:boot_count:") {
let srv_id: String = json_get(srv_node, "id")
if !str_eq(srv_id, "") {
http_delete_json(wb_url + "/api/nodes/" + srv_id, auth_body)
}
}
let si = si + 1
}
}
let wb_body: String = "{\"content\":\"" + content + "\",\"node_type\":\"Memory\",\"label\":\"soul:boot_count\",\"salience\":0.55,\"importance\":0.2,\"tier\":\"Working\",\"tags\":\"[\\\"soul-meta\\\",\\\"boot-counter\\\"]\",\"_auth\":\"" + json_safe(wb_key) + "\"}"
let wb_resp: String = http_post_json(wb_url + "/api/nodes", wb_body)
if str_contains(wb_resp, "\"error\"") {
println("[memory] mem_boot_count_inc: HTTP write-back failed (count in-memory only): " + wb_resp)
}
}
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\"]"
let event_id: String = 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
)
if str_eq(event_id, "") {
println("[memory] mem_emit_state_event: write rejected (empty id): kind=" + kind)
}
return event_id
}