diff --git a/awareness.el b/awareness.el index c2d0a6c..1a1fdb0 100644 --- a/awareness.el +++ b/awareness.el @@ -446,8 +446,10 @@ fn respond(action_json: String) -> String { } if str_eq(kind, "forget") { - engram_forget(payload) - return "{\"outcome\":\"forgotten\",\"id\":\"" + payload + "\"}" + // The soul must NOT be able to autonomously hard-delete a memory. + // Tombstone instead (keep node + edges, recoverable). + let _marker: String = mem_tombstone(payload) + return "{\"outcome\":\"tombstoned\",\"id\":\"" + payload + "\"}" } return "{\"outcome\":\"noop\"}" diff --git a/dist/awareness.c b/dist/awareness.c index 58b4544..e346069 100644 --- a/dist/awareness.c +++ b/dist/awareness.c @@ -10,6 +10,7 @@ el_val_t mem_remember(el_val_t content, el_val_t tags); el_val_t mem_recall(el_val_t query, el_val_t depth); el_val_t mem_search(el_val_t query, el_val_t limit); el_val_t mem_strengthen(el_val_t node_id); +el_val_t mem_tombstone(el_val_t node_id); el_val_t mem_forget(el_val_t node_id); el_val_t mem_consolidate(void); el_val_t mem_save(el_val_t path); @@ -362,8 +363,8 @@ el_val_t respond(el_val_t action_json) { return el_str_concat(el_str_concat(EL_STR("{\"outcome\":\"strengthened\",\"id\":\""), payload), EL_STR("\"}")); } if (str_eq(kind, EL_STR("forget"))) { - engram_forget(payload); - return el_str_concat(el_str_concat(EL_STR("{\"outcome\":\"forgotten\",\"id\":\""), payload), EL_STR("\"}")); + el_val_t _marker = mem_tombstone(payload); + return el_str_concat(el_str_concat(EL_STR("{\"outcome\":\"tombstoned\",\"id\":\""), payload), EL_STR("\"}")); } return EL_STR("{\"outcome\":\"noop\"}"); return 0; diff --git a/dist/memory.c b/dist/memory.c index 148120e..fd77cef 100644 --- a/dist/memory.c +++ b/dist/memory.c @@ -10,6 +10,7 @@ el_val_t mem_remember(el_val_t content, el_val_t tags); el_val_t mem_recall(el_val_t query, el_val_t depth); el_val_t mem_search(el_val_t query, el_val_t limit); el_val_t mem_strengthen(el_val_t node_id); +el_val_t mem_tombstone(el_val_t node_id); el_val_t mem_forget(el_val_t node_id); el_val_t mem_consolidate(void); el_val_t mem_save(el_val_t path); @@ -69,8 +70,18 @@ el_val_t mem_strengthen(el_val_t node_id) { return 0; } +el_val_t mem_tombstone(el_val_t node_id) { + el_val_t tags = EL_STR("[\"Tombstone\",\"status:deleted\"]"); + el_val_t marker = engram_node_full(node_id, EL_STR("Tombstone"), el_str_concat(EL_STR("tombstone:"), node_id), el_from_float(0.01), el_from_float(0.01), el_from_float(1.0), EL_STR("Episodic"), tags); + if (!str_eq(marker, EL_STR(""))) { + engram_connect(marker, node_id, el_from_float(1.0), EL_STR("tombstones")); + } + return marker; + return 0; +} + el_val_t mem_forget(el_val_t node_id) { - engram_forget(node_id); + el_val_t _marker = mem_tombstone(node_id); return 0; } diff --git a/dist/neuron-api.c b/dist/neuron-api.c index eb34141..1a97d4a 100644 --- a/dist/neuron-api.c +++ b/dist/neuron-api.c @@ -10,6 +10,7 @@ el_val_t mem_remember(el_val_t content, el_val_t tags); el_val_t mem_recall(el_val_t query, el_val_t depth); el_val_t mem_search(el_val_t query, el_val_t limit); el_val_t mem_strengthen(el_val_t node_id); +el_val_t mem_tombstone(el_val_t node_id); el_val_t mem_forget(el_val_t node_id); el_val_t mem_consolidate(void); el_val_t mem_save(el_val_t path); @@ -193,12 +194,7 @@ el_val_t api_not_persisted(el_val_t id) { } el_val_t tombstone_node(el_val_t id) { - el_val_t tags = EL_STR("[\"Tombstone\",\"status:deleted\"]"); - el_val_t marker = engram_node_full(id, EL_STR("Tombstone"), el_str_concat(EL_STR("tombstone:"), id), el_from_float(0.01), el_from_float(0.01), el_from_float(1.0), EL_STR("Episodic"), tags); - if (!str_eq(marker, EL_STR(""))) { - engram_connect(marker, id, el_from_float(1.0), EL_STR("tombstones")); - } - return marker; + return mem_tombstone(id); return 0; } @@ -613,7 +609,7 @@ el_val_t handle_api_forget(el_val_t body) { return api_err_protected(node_id); } mem_forget(node_id); - return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), node_id), EL_STR("\"}")); + return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), node_id), EL_STR("\",\"tombstoned\":true}")); return 0; } @@ -717,7 +713,7 @@ el_val_t handle_api_cultivate(el_val_t body) { return api_err(EL_STR("id is required")); } mem_forget(node_id); - return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), node_id), EL_STR("\",\"cultivated\":true}")); + return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), node_id), EL_STR("\",\"tombstoned\":true,\"cultivated\":true}")); } if (str_eq(op, EL_STR("link_entities"))) { el_val_t from_id = json_get(body, EL_STR("from_id")); diff --git a/dist/soul.c b/dist/soul.c index 40cb751..db71e56 100644 --- a/dist/soul.c +++ b/dist/soul.c @@ -963,6 +963,7 @@ el_val_t mem_remember(el_val_t content, el_val_t tags); el_val_t mem_recall(el_val_t query, el_val_t depth); el_val_t mem_search(el_val_t query, el_val_t limit); el_val_t mem_strengthen(el_val_t node_id); +el_val_t mem_tombstone(el_val_t node_id); el_val_t mem_forget(el_val_t node_id); el_val_t mem_consolidate(void); el_val_t mem_save(el_val_t path); @@ -25331,8 +25332,18 @@ el_val_t mem_strengthen(el_val_t node_id) { return 0; } +el_val_t mem_tombstone(el_val_t node_id) { + el_val_t tags = EL_STR("[\"Tombstone\",\"status:deleted\"]"); + el_val_t marker = engram_node_full(node_id, EL_STR("Tombstone"), el_str_concat(EL_STR("tombstone:"), node_id), el_from_float(0.01), el_from_float(0.01), el_from_float(1.0), EL_STR("Episodic"), tags); + if (!str_eq(marker, EL_STR(""))) { + engram_connect(marker, node_id, el_from_float(1.0), EL_STR("tombstones")); + } + return marker; + return 0; +} + el_val_t mem_forget(el_val_t node_id) { - engram_forget(node_id); + el_val_t _marker = mem_tombstone(node_id); return 0; } @@ -26405,8 +26416,8 @@ el_val_t respond(el_val_t action_json) { return el_str_concat(el_str_concat(EL_STR("{\"outcome\":\"strengthened\",\"id\":\""), payload), EL_STR("\"}")); } if (str_eq(kind, EL_STR("forget"))) { - engram_forget(payload); - return el_str_concat(el_str_concat(EL_STR("{\"outcome\":\"forgotten\",\"id\":\""), payload), EL_STR("\"}")); + el_val_t _marker = mem_tombstone(payload); + return el_str_concat(el_str_concat(EL_STR("{\"outcome\":\"tombstoned\",\"id\":\""), payload), EL_STR("\"}")); } return EL_STR("{\"outcome\":\"noop\"}"); return 0; @@ -28728,12 +28739,7 @@ el_val_t api_not_persisted(el_val_t id) { } el_val_t tombstone_node(el_val_t id) { - el_val_t tags = EL_STR("[\"Tombstone\",\"status:deleted\"]"); - el_val_t marker = engram_node_full(id, EL_STR("Tombstone"), el_str_concat(EL_STR("tombstone:"), id), el_from_float(0.01), el_from_float(0.01), el_from_float(1.0), EL_STR("Episodic"), tags); - if (!str_eq(marker, EL_STR(""))) { - engram_connect(marker, id, el_from_float(1.0), EL_STR("tombstones")); - } - return marker; + return mem_tombstone(id); return 0; } @@ -29148,7 +29154,7 @@ el_val_t handle_api_forget(el_val_t body) { return api_err_protected(node_id); } mem_forget(node_id); - return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), node_id), EL_STR("\"}")); + return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), node_id), EL_STR("\",\"tombstoned\":true}")); return 0; } @@ -29252,7 +29258,7 @@ el_val_t handle_api_cultivate(el_val_t body) { return api_err(EL_STR("id is required")); } mem_forget(node_id); - return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), node_id), EL_STR("\",\"cultivated\":true}")); + return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), node_id), EL_STR("\",\"tombstoned\":true,\"cultivated\":true}")); } if (str_eq(op, EL_STR("link_entities"))) { el_val_t from_id = json_get(body, EL_STR("from_id")); diff --git a/mcp-wrapper/src/main.el b/mcp-wrapper/src/main.el index bfb98b6..5c8c46a 100644 --- a/mcp-wrapper/src/main.el +++ b/mcp-wrapper/src/main.el @@ -91,7 +91,7 @@ tool("beginSession", "Initialize session: surface recent high-importance memorie "," + tool("recall", "Retrieve memories by chain or query.") + "," + tool("inspectMemories", "List recent memory nodes.") + "," + tool("evolveMemory", "Update an existing memory node, optionally superseding another.") + -"," + tool("forget", "Remove a node from memory.") + +"," + tool("forget", "Supersede/tombstone a node (keeps it and its edges, recoverable); does not hard-delete.") + "," + tool("pinNode", "Strengthen a node so it stays salient.") + // ── Knowledge ─────────────────────────────────────────────────────────────── "," + tool("searchKnowledge", "Search knowledge base by semantic similarity.") + @@ -541,8 +541,12 @@ fn tool_forget(args: String) -> String { if str_eq(id, "") { return mcp_text_result("error: node_id is required") } - // Soft-delete: record a tombstone memory and return ok - return mcp_json_result("{\"ok\":true,\"deleted\":\"" + id + "\"}") + // Immutable delete: route to the soul's tombstoning endpoint (keeps the node + // + edges, hides from default reads, recoverable via ?include_deleted). + // Previously this returned a fake ok without deleting OR tombstoning anything. + let body: String = "{\"id\":\"" + id + "\"}" + let resp: String = http_post_json(neuron_url() + "/memory/delete", body) + return mcp_json_result(resp) } fn tool_check_events(args: String) -> String { diff --git a/memory.el b/memory.el index bc14505..46d9f28 100644 --- a/memory.el +++ b/memory.el @@ -43,8 +43,32 @@ 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 { - engram_forget(node_id) + let _marker: String = mem_tombstone(node_id) } // mem_consolidate — structural scan plus salience-evolution pass. diff --git a/neuron-api.el b/neuron-api.el index 9fd7982..12ce4c5 100644 --- a/neuron-api.el +++ b/neuron-api.el @@ -114,15 +114,8 @@ fn api_not_persisted(id: String) -> String { // bounded list reads hide tombstoned nodes (memory_hide_tombstoned); internal // cognition and explicit ?include_deleted reads still see them. fn tombstone_node(id: String) -> String { - let tags: String = "[\"Tombstone\",\"status:deleted\"]" - let marker: String = engram_node_full( - id, "Tombstone", "tombstone:" + 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, id, el_from_float(1.0), "tombstones") - } - return marker + // Delegates to the canonical helper in memory.el (single source of truth). + return mem_tombstone(id) } // tombstoned_id_set — delimited "|id1|id2|" of every tombstoned target id. @@ -572,13 +565,15 @@ fn handle_api_link_entities(body: String) -> String { return "{\"ok\":true,\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + eff_relation + "\"}" } -// handle_api_forget — delete a node by ID. Blocked for protected identity nodes. +// handle_api_forget — TOMBSTONE a node by ID (immutable; mem_forget now +// tombstones). The node + edges are kept and recoverable. Blocked for protected +// identity nodes. fn handle_api_forget(body: String) -> String { let node_id: String = json_get(body, "id") if str_eq(node_id, "") { return api_err("id is required") } if is_protected_node(node_id) { return api_err_protected(node_id) } mem_forget(node_id) - return "{\"ok\":true,\"id\":\"" + node_id + "\"}" + return "{\"ok\":true,\"id\":\"" + node_id + "\",\"tombstoned\":true}" } // handle_api_evolve_memory — evolve a Memory node. Blocked for protected identity nodes. @@ -693,8 +688,9 @@ fn handle_api_cultivate(body: String) -> String { if str_eq(op, "forget") { let node_id: String = json_get(body, "id") if str_eq(node_id, "") { return api_err("id is required") } + // Immutable: mem_forget now tombstones (keep node + edges), never hard-delete. mem_forget(node_id) - return "{\"ok\":true,\"id\":\"" + node_id + "\",\"cultivated\":true}" + return "{\"ok\":true,\"id\":\"" + node_id + "\",\"tombstoned\":true,\"cultivated\":true}" } if str_eq(op, "link_entities") {