From dc39a61e2c42dbbd59f7af566f8096e6c01cd755 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Tue, 21 Jul 2026 08:50:38 -0500 Subject: [PATCH] self-review 2026-07-21: stop read routes clobbering canonical snapshot; add /api/load-merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- engram/src/server.el | 49 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 5 deletions(-) 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()))