Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 866c75e5e2 |
@@ -272,89 +272,6 @@ fn route_load_merge(method: String, path: String, body: String) -> String {
|
||||
"{\"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_reseed_nodes — POST /api/nodes/reseed
|
||||
// {"path": "<snapshot-format file>", "replace": ["<id>", ...],
|
||||
// "preserve_edges": true, "_auth": "<key>"}
|
||||
//
|
||||
// ID-PRESERVING install/repair for declarative seed graphs.
|
||||
//
|
||||
// WHY THIS EXISTS (2026-08-10). Two write paths could put a node into the
|
||||
// graph and neither can put a BODY onto an id that already exists:
|
||||
// POST /api/nodes mints a fresh id via engram_node_full, and
|
||||
// POST /api/load-merge honors the declared id but SKIPS anything already
|
||||
// present. That is exactly right for the additive case and leaves one hole:
|
||||
// a node that exists with a truncated body. Forge's genesis seed hit it —
|
||||
// two identity nodes (Voice, Voice Craft) sat in the graph carrying only
|
||||
// their own label as content, 30 and 22 bytes against 4263 and 2590 in the
|
||||
// seed. Their ids are load-bearing (is_protected_node keys on them and 214
|
||||
// declared edges reference them), so "delete and recreate with a new id" is
|
||||
// not a repair, it is a second break.
|
||||
//
|
||||
// Mechanism: engram has no in-place node update, so a replace is
|
||||
// forget-then-merge. engram_forget also drops every INCIDENT EDGE — for
|
||||
// those two nodes that is 85 and 93 edges, almost all of them tag edges and
|
||||
// accumulated hebbian associations that the seed does not declare and could
|
||||
// not restore. preserve_edges (default true) therefore snapshots the graph
|
||||
// before the forget and re-merges that snapshot afterwards: the replaced
|
||||
// node is back by then so it is skipped, and every dropped incident edge
|
||||
// returns through load_merge's (from_id,to_id,relation) dedup. The same
|
||||
// re-merge is the failure path — if the seed merge does not produce the
|
||||
// node, the backup puts the original back. Rollback, not data loss.
|
||||
//
|
||||
// preserve_edges=false skips the two snapshot round-trips (cheap, lossy);
|
||||
// use it only on a graph whose edges are fully declared by the seed.
|
||||
// With no "replace" list this route is exactly /api/load-merge.
|
||||
fn route_reseed_nodes(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 dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||||
let backup: String = dir + "/.reseed-backup.json"
|
||||
|
||||
let replace_raw: String = json_get_raw(body, "replace")
|
||||
let n_replace: Int = json_array_len(replace_raw)
|
||||
// Presence-aware: absent key means "preserve", only an explicit false opts out.
|
||||
let pe_raw: String = json_get_raw(body, "preserve_edges")
|
||||
let preserve: Bool = !str_eq(pe_raw, "false")
|
||||
|
||||
let before_n: Int = engram_node_count()
|
||||
let before_e: Int = engram_edge_count()
|
||||
|
||||
let replaced: Int = 0
|
||||
if n_replace > 0 {
|
||||
if preserve { engram_save(backup) }
|
||||
let i: Int = 0
|
||||
while i < n_replace {
|
||||
let rid: String = json_array_get_string(replace_raw, i)
|
||||
if !str_eq(rid, "") {
|
||||
// engram_get_node_json returns "{}" for a miss — only forget
|
||||
// ids that are actually resident, so a typo in the replace
|
||||
// list is a no-op rather than a silent partial run.
|
||||
let existing: String = engram_get_node_json(rid)
|
||||
if !str_eq(existing, "{}") {
|
||||
engram_forget(rid)
|
||||
let replaced = replaced + 1
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
engram_load_merge(p)
|
||||
if replaced > 0 {
|
||||
if preserve { engram_load_merge(backup) }
|
||||
}
|
||||
|
||||
let saved: Int = persist_canonical()
|
||||
"{\"ok\":true,\"replaced\":" + int_to_str(replaced) +
|
||||
",\"nodes_added\":" + int_to_str(engram_node_count() - before_n) +
|
||||
",\"edges_added\":" + int_to_str(engram_edge_count() - before_e) +
|
||||
",\"node_count\":" + int_to_str(engram_node_count()) +
|
||||
",\"edge_count\":" + int_to_str(engram_edge_count()) + "}"
|
||||
}
|
||||
|
||||
// route_emit_ise — write an InternalStateEvent node from the soul daemon.
|
||||
//
|
||||
// Endpoint: POST /api/neuron/state-events
|
||||
@@ -502,12 +419,6 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
}
|
||||
|
||||
// Nodes
|
||||
// Reseed must be tested before the exact "/api/nodes" match below reads
|
||||
// as the general create path — order is not load-bearing (the match is
|
||||
// exact) but keeping them adjacent keeps them from drifting apart.
|
||||
if str_eq(method, "POST") && (str_eq(clean, "/api/nodes/reseed") || str_eq(clean, "/nodes/reseed")) {
|
||||
return route_reseed_nodes(method, path, body)
|
||||
}
|
||||
if str_eq(method, "POST") && (str_eq(clean, "/api/nodes") || str_eq(clean, "/nodes")) {
|
||||
return route_create_node(method, path, body)
|
||||
}
|
||||
|
||||
@@ -3627,6 +3627,24 @@ fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) ->
|
||||
let pos: Int = 0
|
||||
let el_main_body: [Map<String, Any>] = native_list_empty()
|
||||
let toplevel_exec_stmts: [Map<String, Any>] = native_list_empty()
|
||||
// CGI IDENTITY CAPTURE (2026-08-09). A cgi block is a top-level DECLARATION, so
|
||||
// the classifier below correctly excludes it from toplevel_exec_stmts and calls
|
||||
// el_release on it. The identity emission further down then searched
|
||||
// toplevel_exec_stmts for it — a list that structurally can never contain it —
|
||||
// found nothing, and emitted nothing, silently. Measured: that search sees only
|
||||
// [Let, Expr] for a program whose first statement is a cgi block.
|
||||
// Fix: copy the values out BEFORE the release (strings, so no dangling reference)
|
||||
// and emit from these. No search, so the failure mode is removed rather than moved.
|
||||
let cgi_have: Bool = false
|
||||
let cgi_name_v: String = ""
|
||||
let cgi_did_v: String = ""
|
||||
let cgi_prin_v: String = ""
|
||||
let cgi_net_v: String = ""
|
||||
let cgi_eng_v: String = ""
|
||||
let cgi_has_did: Bool = false
|
||||
let cgi_has_prin: Bool = false
|
||||
let cgi_has_net: Bool = false
|
||||
let cgi_has_eng: Bool = false
|
||||
let has_toplevel_exec: Bool = false
|
||||
|
||||
let stream_running: Bool = true
|
||||
@@ -3737,6 +3755,20 @@ fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) ->
|
||||
if is_top_level_decl(stmt) {
|
||||
// Import, TypeDef, EnumDef, CgiBlock, ServiceBlock, ExternFn
|
||||
// These are no-ops in codegen (forward decls already emitted)
|
||||
// — except a CgiBlock, whose declared identity must survive
|
||||
// this release to be emitted as a compiled constant.
|
||||
if str_eq(sk, "CgiBlock") {
|
||||
let cgi_have = true
|
||||
let cgi_name_v = stmt["name"]
|
||||
let cgi_did_v = stmt["dharma_id"]
|
||||
let cgi_prin_v = stmt["principal"]
|
||||
let cgi_net_v = stmt["network"]
|
||||
let cgi_eng_v = stmt["engram"]
|
||||
let cgi_has_did = stmt["has_dharma_id"]
|
||||
let cgi_has_prin = stmt["has_principal"]
|
||||
let cgi_has_net = stmt["has_network"]
|
||||
let cgi_has_eng = stmt["has_engram"]
|
||||
}
|
||||
el_release(stmt)
|
||||
} else {
|
||||
if str_eq(sk, "Let") {
|
||||
@@ -3815,33 +3847,17 @@ fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) ->
|
||||
let sig2 = native_list_get(sigs, si2)
|
||||
let sk3: String = sig2["kind"]
|
||||
if str_eq(sk3, "cgi_block") {
|
||||
// We need the full cgi_block data — it was parsed by scan_fn_sigs
|
||||
// but scan only stored the name. For cgi_init we need dharma_id etc.
|
||||
// Since cgi blocks are rare and small, they end up in toplevel_exec_stmts.
|
||||
// Find the CgiBlock in toplevel_exec_stmts.
|
||||
let tes_n: Int = native_list_len(toplevel_exec_stmts)
|
||||
let tes_i: Int = 0
|
||||
while tes_i < tes_n {
|
||||
let tes = native_list_get(toplevel_exec_stmts, tes_i)
|
||||
let tes_k: String = tes["stmt"]
|
||||
if str_eq(tes_k, "CgiBlock") {
|
||||
let cname2: String = tes["name"]
|
||||
let cdid2: String = tes["dharma_id"]
|
||||
let cprin2: String = tes["principal"]
|
||||
let cnet2: String = tes["network"]
|
||||
let ceng2: String = tes["engram"]
|
||||
let has_did2: Bool = tes["has_dharma_id"]
|
||||
let has_prin2: Bool = tes["has_principal"]
|
||||
let has_net2: Bool = tes["has_network"]
|
||||
let has_eng2: Bool = tes["has_engram"]
|
||||
let arg_name2: String = "EL_STR(" + c_str_lit(cname2) + ")"
|
||||
let arg_did2: String = cgi_arg(cdid2, has_did2)
|
||||
let arg_prin2: String = cgi_arg(cprin2, has_prin2)
|
||||
let arg_net2: String = cgi_arg(cnet2, has_net2)
|
||||
let arg_eng2: String = cgi_arg(ceng2, has_eng2)
|
||||
emit_line(" el_cgi_init(" + arg_name2 + ", " + arg_did2 + ", " + arg_prin2 + ", " + arg_net2 + ", " + arg_eng2 + ");")
|
||||
}
|
||||
let tes_i = tes_i + 1
|
||||
// Emit from the values captured before the declaration was released.
|
||||
// The previous implementation searched toplevel_exec_stmts, which by
|
||||
// construction never contains a declaration — so it emitted nothing and
|
||||
// said nothing. See the capture block near toplevel_exec_stmts init.
|
||||
if cgi_have {
|
||||
let arg_name2: String = "EL_STR(" + c_str_lit(cgi_name_v) + ")"
|
||||
let arg_did2: String = cgi_arg(cgi_did_v, cgi_has_did)
|
||||
let arg_prin2: String = cgi_arg(cgi_prin_v, cgi_has_prin)
|
||||
let arg_net2: String = cgi_arg(cgi_net_v, cgi_has_net)
|
||||
let arg_eng2: String = cgi_arg(cgi_eng_v, cgi_has_eng)
|
||||
emit_line(" el_cgi_init(" + arg_name2 + ", " + arg_did2 + ", " + arg_prin2 + ", " + arg_net2 + ", " + arg_eng2 + ");")
|
||||
}
|
||||
}
|
||||
let si2 = si2 + 1
|
||||
|
||||
Reference in New Issue
Block a user