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:", 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 }