prevent engram corruption: idempotent boot seeding, session-start event cap
Fix 1: mem_boot_count_inc prunes all existing soul:boot_count nodes before
inserting the new one — keeps exactly one boot counter node instead
of accumulating a new node per boot. Also fixes a latent ordering
bug where engram_search_json oldest-first results caused the counter
to read stale (low) values once >3 copies accumulated.
Fix 3: handle_api_node_delete comment clarified — the no-verify exception
is correct for deletes (not a write path); read-back-verify is for
writes only.
Fix 4: emit_session_start_event prunes old session-start InternalStateEvent
nodes after each boot, keeping the 10 most recent and forgetting
older ones. Prevents unbounded accumulation of ~120+ copies.
This commit is contained in:
+43
-1
@@ -25343,9 +25343,31 @@ el_val_t mem_boot_count_get(void) {
|
||||
el_val_t mem_boot_count_inc(void) {
|
||||
el_val_t current = mem_boot_count_get();
|
||||
el_val_t next = (current + 1);
|
||||
/* Prune all existing soul:boot_count nodes — keep exactly one. */
|
||||
el_val_t old_results = engram_search_json(EL_STR("soul:boot_count"), 50);
|
||||
if (!str_eq(old_results, EL_STR("")) && !str_eq(old_results, EL_STR("[]"))) {
|
||||
el_val_t old_len = json_array_len(old_results);
|
||||
el_val_t oi = 0;
|
||||
while (oi < old_len) {
|
||||
el_val_t old_node = json_array_get(old_results, oi);
|
||||
el_val_t old_id = json_get(old_node, EL_STR("id"));
|
||||
if (!str_eq(old_id, EL_STR(""))) {
|
||||
(void)(engram_forget(old_id));
|
||||
}
|
||||
oi = (oi + 1);
|
||||
}
|
||||
}
|
||||
el_val_t content = el_str_concat(EL_STR("soul:boot_count:"), int_to_str(next));
|
||||
el_val_t tags = EL_STR("[\"soul-meta\",\"boot-counter\"]");
|
||||
el_val_t discard = engram_node_full(content, EL_STR("Memory"), EL_STR("soul:boot_count"), el_from_float(el_from_float(0.9)), el_from_float(el_from_float(0.9)), el_from_float(el_from_float(1.0)), EL_STR("Canonical"), tags);
|
||||
el_val_t boot_node_id = engram_node_full(content, EL_STR("Memory"), EL_STR("soul:boot_count"), el_from_float(el_from_float(0.9)), el_from_float(el_from_float(0.9)), el_from_float(el_from_float(1.0)), EL_STR("Canonical"), tags);
|
||||
if (str_eq(boot_node_id, EL_STR(""))) {
|
||||
println(el_str_concat(el_str_concat(EL_STR("[memory] mem_boot_count_inc: write rejected (empty id) — boot counter node lost (count="), int_to_str(next)), EL_STR(")")));
|
||||
return next;
|
||||
}
|
||||
el_val_t boot_readback = engram_get_node_json(boot_node_id);
|
||||
if (str_eq(boot_readback, EL_STR("")) || str_eq(boot_readback, EL_STR("{}"))) {
|
||||
println(el_str_concat(el_str_concat(el_str_concat(EL_STR("[memory] mem_boot_count_inc: WRITE VERIFY FAILED id="), boot_node_id), EL_STR(" count=")), int_to_str(next)));
|
||||
}
|
||||
return next;
|
||||
return 0;
|
||||
}
|
||||
@@ -29421,6 +29443,26 @@ el_val_t emit_session_start_event(void) {
|
||||
el_val_t payload = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"event\":\"session_start\""), EL_STR(",\"boot\":")), boot_num), EL_STR(",\"cgi\":\"")), eff_cgi), EL_STR("\"")), EL_STR(",\"node_count\":")), int_to_str(node_ct)), EL_STR(",\"edge_count\":")), int_to_str(edge_ct)), EL_STR(",\"identity_loaded\":")), has_identity), EL_STR(",\"ts\":")), int_to_str(ts)), EL_STR("}"));
|
||||
el_val_t tags = EL_STR("[\"internal-state\",\"session-start\",\"InternalStateEvent\"]");
|
||||
el_val_t discard = engram_node_full(payload, EL_STR("InternalStateEvent"), EL_STR("session-start"), el_from_float(el_from_float(0.9)), el_from_float(el_from_float(0.9)), el_from_float(el_from_float(1.0)), EL_STR("Episodic"), tags);
|
||||
/* Prune accumulated session-start events — keep the 10 most recent.
|
||||
* engram_search_json returns oldest-first, so forget from index 0 to (count-11). */
|
||||
el_val_t keep_n = 10;
|
||||
el_val_t old_events = engram_search_json(EL_STR("session-start InternalStateEvent"), 200);
|
||||
if (!str_eq(old_events, EL_STR("")) && !str_eq(old_events, EL_STR("[]"))) {
|
||||
el_val_t ev_count = json_array_len(old_events);
|
||||
if (ev_count > keep_n) {
|
||||
el_val_t prune_to = (ev_count - keep_n);
|
||||
el_val_t ei = 0;
|
||||
while (ei < prune_to) {
|
||||
el_val_t old_ev = json_array_get(old_events, ei);
|
||||
el_val_t old_ev_id = json_get(old_ev, EL_STR("id"));
|
||||
if (!str_eq(old_ev_id, EL_STR(""))) {
|
||||
(void)(engram_forget(old_ev_id));
|
||||
}
|
||||
ei = (ei + 1);
|
||||
}
|
||||
println(el_str_concat(el_str_concat(el_str_concat(EL_STR("[soul] pruned "), int_to_str(prune_to)), EL_STR(" old session-start events (kept 10)")), EL_STR("")));
|
||||
}
|
||||
}
|
||||
println(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("[soul] session-start event logged (boot="), boot_num), EL_STR(" nodes=")), int_to_str(node_ct)), EL_STR(" edges=")), int_to_str(edge_ct)), EL_STR(")")));
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -134,12 +134,30 @@ fn mem_boot_count_get() -> Int {
|
||||
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.
|
||||
// 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\"]"
|
||||
let boot_node_id: String = engram_node_full(
|
||||
|
||||
+6
-5
@@ -196,11 +196,12 @@ fn handle_api_node_create(body: String) -> String {
|
||||
fn handle_api_node_delete(body: String) -> String {
|
||||
let id: String = json_get(body, "id")
|
||||
if str_eq(id, "") { return api_err("id is required") }
|
||||
// engram_forget removes the node + its incident edges from the live graph. We do
|
||||
// NOT read-back-verify here: engram_get_node_json can return a STALE hit for a just-
|
||||
// removed id (the id->index map is not rebuilt on forget), which would produce a
|
||||
// false "delete_failed" even though the node is gone. The graph endpoints
|
||||
// (/api/graph/nodes) correctly reflect the removal, which is the source of truth.
|
||||
// engram_forget removes the node + its incident edges from the live graph.
|
||||
// Delete is NOT read-back-verified: engram_get_node_json can return a stale hit
|
||||
// for a just-forgotten id because the id→index map is not rebuilt on forget.
|
||||
// A stale hit would cause a false "delete_failed" on a successful deletion.
|
||||
// This exception is correct: read-back-verify guards WRITES; for deletes,
|
||||
// the graph endpoints (/api/graph/nodes) reflect the removal and are the source of truth.
|
||||
engram_forget(id)
|
||||
return "{\"ok\":true,\"id\":\"" + id + "\"}"
|
||||
}
|
||||
|
||||
@@ -346,6 +346,27 @@ fn emit_session_start_event() -> Void {
|
||||
el_from_float(0.9), el_from_float(0.9), el_from_float(1.0),
|
||||
"Episodic", tags
|
||||
)
|
||||
// Prune accumulated session-start events — keep the 10 most recent.
|
||||
// engram_search_json returns results in insertion order (oldest first), so
|
||||
// results[0..count-11] are the oldest; forgetting them leaves the newest 10.
|
||||
let keep_n: Int = 10
|
||||
let old_events: String = engram_search_json("session-start InternalStateEvent", 200)
|
||||
if !str_eq(old_events, "") && !str_eq(old_events, "[]") {
|
||||
let ev_count: Int = json_array_len(old_events)
|
||||
if ev_count > keep_n {
|
||||
let prune_to: Int = ev_count - keep_n
|
||||
let ei: Int = 0
|
||||
while ei < prune_to {
|
||||
let old_ev: String = json_array_get(old_events, ei)
|
||||
let old_ev_id: String = json_get(old_ev, "id")
|
||||
if !str_eq(old_ev_id, "") {
|
||||
engram_forget(old_ev_id)
|
||||
}
|
||||
let ei = ei + 1
|
||||
}
|
||||
println("[soul] pruned " + int_to_str(prune_to) + " old session-start events (kept " + int_to_str(keep_n) + ")")
|
||||
}
|
||||
}
|
||||
println("[soul] session-start event logged (boot=" + boot_num + " nodes=" + int_to_str(node_ct) + " edges=" + int_to_str(edge_ct) + " prev_summary=" + has_prev_sum + ")")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user