diff --git a/soul.el b/soul.el index 1f1cdc7..3a9b489 100644 --- a/soul.el +++ b/soul.el @@ -584,17 +584,35 @@ println("[soul] boot - cgi=" + soul_cgi_id + " port=" + int_to_str(port)) let using_http_engram: Bool = !str_eq(engram_url_raw, "") -// Always try local snapshot first. If it has content (>50 nodes) it was -// previously seeded from HTTP Engram and is kept up-to-date by the awareness -// loop — use it. This preserves sessions and memories across restarts. -// HTTP Engram is only used for the very first boot (empty/absent snapshot). -engram_load(snapshot) -let local_node_count: Int = engram_node_count() -let snapshot_usable: Bool = local_node_count > 50 - -if using_http_engram && !snapshot_usable { - // First boot or empty/corrupt snapshot: seed from HTTP Engram. - println("[soul] engram -> HTTP " + engram_url_raw + " (no local snapshot, first boot)") +// THE ENGRAM IS THE CANONICAL STORE. The soul's resident graph is a working +// copy of it, never a rival source of truth. +// +// This used to be inverted: "always try local snapshot first... HTTP Engram is +// only used for the very first boot." The copy outranked the store. Everything +// that followed is a cost of that one inversion: +// - the two graphs drifted (13,479 nodes/74,563 edges in the soul vs +// 13,425/37,658 in the engram — nearly 2x the edges, silently) +// - write-through exists solely to reconcile them, and had never once run +// - /api/graph/edges serialized 128 MB to answer a read, because the soul's +// copy was not the engram's +// - a read route overwrote the engram's canonical snapshot.json with the +// soul's divergent copy +// - three copies of the same memory: neuron.egm, snapshot.json, soul RAM +// None of those are features. They are all reconciliation debt. +// +// The engram itself already reached this conclusion for its own boot path: +// "the durable owner is the paged store (neuron.egm + neuron.wal) ... +// snapshot.json is never read again as the ongoing store. This closes the +// 'restart reverted to a 17h-old snapshot' data-loss window." The soul kept +// booting the legacy way the engram had already abandoned, and inherited +// exactly the data-loss window that comment describes. +// +// So in HTTP-engram mode the soul seeds from the engram, EVERY boot, and never +// consults or writes a local snapshot. The local file is not read even when +// present — a stale copy that outranks the store is the bug, not a fallback. +// (File mode, no ENGRAM_URL, is unchanged: there the soul IS the owner.) +if using_http_engram { + println("[soul] engram -> HTTP " + engram_url_raw + " (canonical store; local snapshot ignored)") let nodes_json: String = http_get(engram_url_raw + "/api/nodes?limit=10000") let edges_json: String = http_get(engram_url_raw + "/api/edges") let nodes_part: String = if str_eq(nodes_json, "") { "[]" } else { nodes_json } @@ -603,9 +621,21 @@ if using_http_engram && !snapshot_usable { let tmp_path: String = "/tmp/soul-engram-" + soul_cgi_id + ".json" fs_write(tmp_path, snapshot_data) engram_load(tmp_path) - println("[soul] loaded from HTTP Engram - nodes=" + int_to_str(engram_node_count()) + " edges=" + int_to_str(engram_edge_count())) + let seeded: Int = engram_node_count() + if seeded < 50 { + // Refuse to run blind. An empty seed in HTTP mode means the canonical + // store was unreachable or empty; continuing would let the soul rebuild + // a divergent graph from nothing, which is how the copies split before. + println("[soul] FATAL: engram at " + engram_url_raw + " returned " + int_to_str(seeded) + + " nodes. The canonical store is unreachable or empty; refusing to boot on a" + + " local copy. Fix the engram, then restart.") + exit_program(1) + } + println("[soul] loaded from engram - nodes=" + int_to_str(seeded) + " edges=" + int_to_str(engram_edge_count())) } else { - println("[soul] loaded from local snapshot - nodes=" + int_to_str(local_node_count) + " edges=" + int_to_str(engram_edge_count())) + engram_load(snapshot) + println("[soul] file mode (no ENGRAM_URL) - soul owns the store - nodes=" + + int_to_str(engram_node_count()) + " edges=" + int_to_str(engram_edge_count())) } load_identity_context()