diff --git a/engram/src/server.el b/engram/src/server.el index da54fc6..3e6318b 100644 --- a/engram/src/server.el +++ b/engram/src/server.el @@ -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 -// via engram_save → fs_read of the canonical on-disk snapshot, which the -// runtime keeps in lockstep with the in-memory graph. Live against the -// running graph, not a stale export. +// via engram_save → fs_read of a SCRATCH export path. (2026-07-21 self-review: +// previously this saved over the canonical snapshot.json on every GET — if the +// 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 { let dir_raw: String = env("ENGRAM_DATA_DIR") 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) let snap: String = fs_read(snap_path) 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 { let dir_raw: String = env("ENGRAM_DATA_DIR") 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) let snap: String = fs_read(snap_path) if str_eq(snap, "") { return "{\"nodes\":[],\"edges\":[]}" } 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. // // 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")) { 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 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" 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] data_dir=" + data_dir) println("[engram] node_count=" + int_to_str(engram_node_count()))