self-review 2026-07-21: stop read routes clobbering canonical snapshot; add /api/load-merge
Root cause of the 2026-05→07 identity-node loss: route_scan_edges and route_sync serialized state by engram_save()ing over the canonical snapshot.json on every GET, so one bad boot load meant the first read request overwrote the good snapshot. Read routes now export to scratch paths. Boot guard preserves evidence on non-empty-file/zero-node loads and keeps a boot-time backup on good loads. New POST /api/load-merge (explicit path required) used to restore 385 identity nodes + 1115 edges from the 2026-05-13 backup.
This commit is contained in:
+44
-5
@@ -112,13 +112,14 @@ fn route_scan_nodes(method: String, path: String, body: String) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// route_scan_edges — bulk export of all edges as a JSON array. Implemented
|
// route_scan_edges — bulk export of all edges as a JSON array. Implemented
|
||||||
// via engram_save → fs_read of the canonical on-disk snapshot, which the
|
// via engram_save → fs_read of a SCRATCH export path. (2026-07-21 self-review:
|
||||||
// runtime keeps in lockstep with the in-memory graph. Live against the
|
// previously this saved over the canonical snapshot.json on every GET — if the
|
||||||
// running graph, not a stale export.
|
// process ever booted with a partial/empty store, the first read request
|
||||||
|
// clobbered the good snapshot. Read routes must never write the canonical path.)
|
||||||
fn route_scan_edges(method: String, path: String, body: String) -> String {
|
fn route_scan_edges(method: String, path: String, body: String) -> String {
|
||||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||||
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||||||
let snap_path: String = dir + "/snapshot.json"
|
let snap_path: String = dir + "/.scan-export.json"
|
||||||
engram_save(snap_path)
|
engram_save(snap_path)
|
||||||
let snap: String = fs_read(snap_path)
|
let snap: String = fs_read(snap_path)
|
||||||
if str_eq(snap, "") { return "[]" }
|
if str_eq(snap, "") { return "[]" }
|
||||||
@@ -219,13 +220,33 @@ fn route_health(method: String, path: String, body: String) -> String {
|
|||||||
fn route_sync(method: String, path: String, body: String) -> String {
|
fn route_sync(method: String, path: String, body: String) -> String {
|
||||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||||
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||||||
let snap_path: String = dir + "/snapshot.json"
|
// 2026-07-21 self-review: export to a scratch path, never the canonical
|
||||||
|
// snapshot.json — read routes must not be able to clobber the good snapshot.
|
||||||
|
let snap_path: String = dir + "/.sync-export.json"
|
||||||
engram_save(snap_path)
|
engram_save(snap_path)
|
||||||
let snap: String = fs_read(snap_path)
|
let snap: String = fs_read(snap_path)
|
||||||
if str_eq(snap, "") { return "{\"nodes\":[],\"edges\":[]}" }
|
if str_eq(snap, "") { return "{\"nodes\":[],\"edges\":[]}" }
|
||||||
return snap
|
return snap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// route_load_merge — POST /api/load-merge {"path": "..."} — merge a snapshot
|
||||||
|
// file into the live store WITHOUT resetting it (engram_load_merge skips nodes
|
||||||
|
// already present by id). Added 2026-07-21 self-review to restore the 244 kn-
|
||||||
|
// identity Knowledge nodes lost from the snapshot lineage between 05-13 and
|
||||||
|
// 07-13. Requires an explicit path: refuses to run without one so it can never
|
||||||
|
// be triggered accidentally against a default.
|
||||||
|
fn route_load_merge(method: String, path: String, body: String) -> String {
|
||||||
|
let p: String = json_get_string(body, "path")
|
||||||
|
if str_eq(p, "") { return err_json("path is required") }
|
||||||
|
if str_eq(fs_read(p), "") { return err_json("file missing or empty") }
|
||||||
|
let before_n: Int = engram_node_count()
|
||||||
|
let before_e: Int = engram_edge_count()
|
||||||
|
engram_load_merge(p)
|
||||||
|
let added_n: Int = engram_node_count() - before_n
|
||||||
|
let added_e: Int = engram_edge_count() - before_e
|
||||||
|
"{\"ok\":true,\"nodes_added\":" + int_to_str(added_n) + ",\"edges_added\":" + int_to_str(added_e) + ",\"node_count\":" + int_to_str(engram_node_count()) + "}"
|
||||||
|
}
|
||||||
|
|
||||||
// route_emit_ise — write an InternalStateEvent node from the soul daemon.
|
// route_emit_ise — write an InternalStateEvent node from the soul daemon.
|
||||||
//
|
//
|
||||||
// Endpoint: POST /api/neuron/state-events
|
// Endpoint: POST /api/neuron/state-events
|
||||||
@@ -422,6 +443,9 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
|||||||
if str_eq(method, "POST") && (str_eq(clean, "/api/load") || str_eq(clean, "/load")) {
|
if str_eq(method, "POST") && (str_eq(clean, "/api/load") || str_eq(clean, "/load")) {
|
||||||
return route_load(method, path, body)
|
return route_load(method, path, body)
|
||||||
}
|
}
|
||||||
|
if str_eq(method, "POST") && (str_eq(clean, "/api/load-merge") || str_eq(clean, "/load-merge")) {
|
||||||
|
return route_load_merge(method, path, body)
|
||||||
|
}
|
||||||
|
|
||||||
// Sync — soul daemon periodic pull of non-ISE knowledge into in-process graph
|
// Sync — soul daemon periodic pull of non-ISE knowledge into in-process graph
|
||||||
if str_eq(method, "GET") && str_eq(clean, "/api/sync") {
|
if str_eq(method, "GET") && str_eq(clean, "/api/sync") {
|
||||||
@@ -443,6 +467,21 @@ let data_dir: String = if str_eq(data_dir_raw, "") { "/tmp/engram" } else { data
|
|||||||
let snapshot_path: String = data_dir + "/snapshot.json"
|
let snapshot_path: String = data_dir + "/snapshot.json"
|
||||||
engram_load(snapshot_path)
|
engram_load(snapshot_path)
|
||||||
|
|
||||||
|
// 2026-07-21 self-review boot guard: if the snapshot file has content but the
|
||||||
|
// load produced 0 nodes, something is wrong (corrupt file / parse failure).
|
||||||
|
// Preserve the evidence and warn loudly — and since read routes no longer write
|
||||||
|
// the canonical path, a bad boot can no longer clobber the good snapshot.
|
||||||
|
let boot_snap: String = fs_read(snapshot_path)
|
||||||
|
if !str_eq(boot_snap, "") {
|
||||||
|
if engram_node_count() == 0 {
|
||||||
|
println("[engram] WARNING: snapshot.json is non-empty but load produced 0 nodes — preserving copy at snapshot.failed-load.json")
|
||||||
|
fs_write(data_dir + "/snapshot.failed-load.json", boot_snap)
|
||||||
|
} else {
|
||||||
|
// Good load: keep a boot-time backup of the snapshot as loaded.
|
||||||
|
fs_write(data_dir + "/snapshot.boot-backup.json", boot_snap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
println("[engram] runtime-native graph engine")
|
println("[engram] runtime-native graph engine")
|
||||||
println("[engram] data_dir=" + data_dir)
|
println("[engram] data_dir=" + data_dir)
|
||||||
println("[engram] node_count=" + int_to_str(engram_node_count()))
|
println("[engram] node_count=" + int_to_str(engram_node_count()))
|
||||||
|
|||||||
Reference in New Issue
Block a user