Compare commits

...

5 Commits

Author SHA1 Message Date
will.anderson 392d2416ec fix(reliability): replace undefined session_exists with session_get check
Neuron Soul CI / build (pull_request) Failing after 13m25s
2026-06-22 12:21:31 -05:00
will.anderson 494d973a3b fix(reliability): engram-write — guard all fire-and-forget writes
Neuron Soul CI / build (pull_request) Has been cancelled
Every engram_node_full call that dropped its return value now binds it
and emits a println on empty string. engram_save calls in consolidate,
heartbeat, and dharma-room-turn are checked for failure. The two API
handlers (log_state_event, tune_config) that skipped api_persisted()
now match the read-back-after-write contract used everywhere else in
neuron-api.el.

Files changed:
- chat.el: conv_history_persist, handle_dharma_room_turn, auto_persist
- soul.el: emit_session_start_event, seed_persona_from_env HTTP check
- memory.el: mem_save, mem_boot_count_inc
- neuron-api.el: handle_api_log_state_event, handle_api_tune_config,
  handle_api_consolidate (engram_save + session summary write)
- awareness.el: ise_post local-engram fallback path

TODO comments added for non-atomic patterns (issues #12, #13) and
the missing circuit breaker (#14) — these require new primitives.
2026-06-22 11:48:59 -05:00
will.anderson ddd858d2ec fix(deploy): extend rollout timeout to 8m for GKE Autopilot cold starts
Neuron Soul CI / build (push) Has been cancelled
Deploy Soul to GKE / deploy (push) Failing after 5m48s
2026-06-19 15:35:34 -05:00
will.anderson 996dd3860a fix: replace embedded python with sed in deploy-gke manifest update step
Neuron Soul CI / build (push) Successful in 7m6s
Deploy Soul to GKE / deploy (push) Failing after 8m11s
2026-06-19 15:25:22 -05:00
will.anderson 6f4adf7640 self-review 2026-06-19: filter auto_term to Memory/BacklogItem/Entity only
Knowledge nodes dominated the WM-autobiographical auto_term slot:
'Numeric tier strings...' (a Knowledge node) always scored highest
in WM and its first word 'Numeric' became the curiosity seed every
scan — activating more Numeric nodes, keeping that node in WM,
repeating indefinitely.

Fix: only derive auto_term from Memory, BacklogItem, or Entity nodes.
Knowledge nodes are reference material, not live context. Dynamic/
personal nodes carry the salience worth radiating from.

Also patches proactive_curiosity directly in dist/neuron.c (ELC
cannot compile soul.el within timeout — fallback build pattern).
2026-06-19 08:49:42 -05:00
8 changed files with 164 additions and 45 deletions
+5 -18
View File
@@ -214,23 +214,10 @@ jobs:
cd /tmp/infra-update cd /tmp/infra-update
DEPLOY_DIR="platform/k8s/neuron-mcp" DEPLOY_DIR="platform/k8s/neuron-mcp"
python3 -c " sed -i "s/^ replicas: .*/ replicas: 1/" "${DEPLOY_DIR}/deployment-${SLOT}.yaml"
import re, sys sed -i "s/^ replicas: .*/ replicas: 0/" "${DEPLOY_DIR}/deployment-${IDLE}.yaml"
echo " deployment-${SLOT}.yaml: replicas set to 1"
slot = sys.argv[1] echo " deployment-${IDLE}.yaml: replicas set to 0"
idle = sys.argv[2]
def set_replicas(path, count):
with open(path) as f:
content = f.read()
content = re.sub(r'^( replicas: )\d+', r'\g<1>' + str(count), content, count=1, flags=re.MULTILINE)
with open(path, 'w') as f:
f.write(content)
print(f' {path}: replicas set to {count}')
set_replicas(f'{DEPLOY_DIR}/deployment-{slot}.yaml', 1)
set_replicas(f'{DEPLOY_DIR}/deployment-{idle}.yaml', 0)
" "$SLOT" "$IDLE"
git config user.email "ci@neurontechnologies.ai" git config user.email "ci@neurontechnologies.ai"
git config user.name "Neuron CI" git config user.name "Neuron CI"
@@ -246,7 +233,7 @@ set_replicas(f'{DEPLOY_DIR}/deployment-{idle}.yaml', 0)
echo "Verifying neuron-mcp-${SLOT} is healthy..." echo "Verifying neuron-mcp-${SLOT} is healthy..."
kubectl rollout status deployment/"neuron-mcp-${SLOT}" \ kubectl rollout status deployment/"neuron-mcp-${SLOT}" \
--namespace=neuron-prod \ --namespace=neuron-prod \
--timeout=3m --timeout=8m
echo "Active service endpoints:" echo "Active service endpoints:"
kubectl get endpoints neuron-mcp -n neuron-prod kubectl get endpoints neuron-mcp -n neuron-prod
+26 -6
View File
@@ -23,11 +23,14 @@ fn ise_post(content: String) -> Void {
let ise_url: String = env("SOUL_ISE_URL") let ise_url: String = env("SOUL_ISE_URL")
let engram_url: String = if str_eq(ise_url, "") { state_get("soul_engram_url") } else { ise_url } let engram_url: String = if str_eq(ise_url, "") { state_get("soul_engram_url") } else { ise_url }
if str_eq(engram_url, "") { if str_eq(engram_url, "") {
let discard: String = engram_node_full( let local_id: String = engram_node_full(
content, "InternalStateEvent", "state-event", content, "InternalStateEvent", "state-event",
el_from_float(0.3), el_from_float(0.3), el_from_float(0.8), el_from_float(0.3), el_from_float(0.3), el_from_float(0.8),
"Episodic", "[\"internal-state\",\"InternalStateEvent\"]" "Episodic", "[\"internal-state\",\"InternalStateEvent\"]"
) )
if str_eq(local_id, "") {
println("[awareness] ise_post: local engram_node_full failed — ISE lost")
}
return "" return ""
} }
// Proper JSON string escaping: backslashes first, then quotes, then control chars. // Proper JSON string escaping: backslashes first, then quotes, then control chars.
@@ -219,15 +222,32 @@ fn proactive_curiosity() -> Bool {
// str_find_chars finds the first space/colon/bracket delimiter. sp > 3 guards against // str_find_chars finds the first space/colon/bracket delimiter. sp > 3 guards against
// very short or bracket-prefixed labels like "[BacklogItem]" (sp=0, not > 3 → skipped). // very short or bracket-prefixed labels like "[BacklogItem]" (sp=0, not > 3 → skipped).
// EL scoping: state_set/state_get pattern used because let inside if creates inner scope. // EL scoping: state_set/state_get pattern used because let inside if creates inner scope.
// (2026-06-11 self-review) //
// NODE TYPE FILTER (2026-06-19 self-review): only derive auto_term from Memory,
// BacklogItem, or Entity nodes. Knowledge nodes are stable reference material —
// using their first word as a curiosity seed creates a self-reinforcing loop: e.g.
// "Numeric tier strings in Engram..." (a Knowledge node) -> auto_term="Numeric" ->
// activates all "Numeric" nodes -> keeps that Knowledge node dominant in WM forever.
// Knowledge nodes should be REACHED by curiosity seeds, not drive them. Only dynamic
// personal/work nodes (Memory, BacklogItem, Entity) carry live contextual salience
// worth radiating from. (2026-06-11 origin; filter added 2026-06-19 self-review)
state_set("cseed_auto", "") state_set("cseed_auto", "")
let wm_top_j: String = engram_wm_top_json(1) let wm_top_j: String = engram_wm_top_json(1)
let wm_top_n: String = json_array_get(wm_top_j, 0) let wm_top_n: String = json_array_get(wm_top_j, 0)
let wm_top_lbl: String = json_get(wm_top_n, "label") let wm_top_lbl: String = json_get(wm_top_n, "label")
if !str_eq(wm_top_lbl, "") { let wm_top_type: String = json_get(wm_top_n, "node_type")
let sp: Int = str_find_chars(wm_top_lbl, " :([") // state_set/state_get pattern: EL let-inside-if creates inner scope only.
if sp > 3 { state_set("allow_auto", "0")
state_set("cseed_auto", str_slice(wm_top_lbl, 0, sp)) if str_eq(wm_top_type, "Memory") { state_set("allow_auto", "1") }
if str_eq(wm_top_type, "BacklogItem") { state_set("allow_auto", "1") }
if str_eq(wm_top_type, "Entity") { state_set("allow_auto", "1") }
let allow_auto: String = state_get("allow_auto")
if str_eq(allow_auto, "1") {
if !str_eq(wm_top_lbl, "") {
let sp: Int = str_find_chars(wm_top_lbl, " :([")
if sp > 3 {
state_set("cseed_auto", str_slice(wm_top_lbl, 0, sp))
}
} }
} }
let auto_term: String = state_get("cseed_auto") let auto_term: String = state_get("cseed_auto")
+31 -4
View File
@@ -130,11 +130,14 @@ fn conv_history_persist(hist: String) -> Void {
if str_eq(hist, "[]") { return "" } if str_eq(hist, "[]") { return "" }
let ts: Int = time_now() let ts: Int = time_now()
let tags: String = "[\"conv-history\",\"persistent\"]" let tags: String = "[\"conv-history\",\"persistent\"]"
let discard: String = engram_node_full( let node_id: String = engram_node_full(
hist, "Conversation", "conv:history", hist, "Conversation", "conv:history",
el_from_float(0.7), el_from_float(0.8), el_from_float(0.9), el_from_float(0.7), el_from_float(0.8), el_from_float(0.9),
"Episodic", tags "Episodic", tags
) )
if str_eq(node_id, "") {
println("[chat] conv_history_persist: engram_node_full returned empty — history node lost")
}
} }
// conv_history_load restore conversation history from engram on first access. // conv_history_load restore conversation history from engram on first access.
@@ -637,6 +640,21 @@ fn handle_chat_agentic(body: String) -> String {
// Thread-aware activation: same logic as handle_chat. // Thread-aware activation: same logic as handle_chat.
// Use the session's or global history to anchor short messages to the thread. // Use the session's or global history to anchor short messages to the thread.
let req_session: String = json_get(body, "session_id") let req_session: String = json_get(body, "session_id")
// ISSUE #6/#7: validate that the session_id actually exists before proceeding.
// Without this check the loop silently treats any unknown/fabricated session_id
// as a fresh session history loads as empty and no error is returned to the caller.
// Only validate when a session_id is explicitly provided; anonymous calls
// (no session_id) continue to work for backward compatibility.
let session_valid: Bool = if str_eq(req_session, "") {
true
} else {
!str_contains(session_get(req_session), "\"error\"")
}
if !session_valid {
return "{\"error\":\"session not found\",\"session_id\":\"" + req_session + "\",\"reply\":\"\"}"
}
let hist_key: String = if str_eq(req_session, "") { "conv_history" } else { "session_hist_" + req_session } let hist_key: String = if str_eq(req_session, "") { "conv_history" } else { "session_hist_" + req_session }
let agentic_hist: String = state_get(hist_key) let agentic_hist: String = state_get(hist_key)
let agentic_hist_len: Int = if str_eq(agentic_hist, "") { 0 } else { json_array_len(agentic_hist) } let agentic_hist_len: Int = if str_eq(agentic_hist, "") { 0 } else { json_array_len(agentic_hist) }
@@ -1054,13 +1072,19 @@ fn handle_dharma_room_turn(body: String) -> String {
// engram_node(content, "episodic", ...) which wrongly put a TIER into the node_type // engram_node(content, "episodic", ...) which wrongly put a TIER into the node_type
// slot that's why nodes showed node_type="episodic". Use the full, correct contract.) // slot that's why nodes showed node_type="episodic". Use the full, correct contract.)
let utterance_tags: String = "[\"soul-utterance\",\"episodic\"]" let utterance_tags: String = "[\"soul-utterance\",\"episodic\"]"
let discard_id: String = engram_node_full( let utterance_id: String = engram_node_full(
clean_response, "Conversation", "soul:utterance", clean_response, "Conversation", "soul:utterance",
el_from_float(0.6), el_from_float(0.6), el_from_float(0.8), el_from_float(0.6), el_from_float(0.6), el_from_float(0.8),
"Episodic", utterance_tags "Episodic", utterance_tags
) )
if str_eq(utterance_id, "") {
println("[chat] handle_dharma_room_turn: utterance engram write failed — node lost")
}
if !str_eq(snap_path, "") { if !str_eq(snap_path, "") {
let discard_save: String = engram_save(snap_path) let save_result: String = engram_save(snap_path)
if str_eq(save_result, "") {
println("[chat] handle_dharma_room_turn: engram_save failed for " + snap_path)
}
} }
let safe_response: String = json_safe(clean_response) let safe_response: String = json_safe(clean_response)
@@ -1142,7 +1166,7 @@ fn auto_persist(req: String, resp: String) -> Void {
+ ",\"label\":\"chat:" + ts_str + "\"}" + ",\"label\":\"chat:" + ts_str + "\"}"
let tags: String = "[\"Conversation\",\"chat\",\"timestamped\"]" let tags: String = "[\"Conversation\",\"chat\",\"timestamped\"]"
engram_node_full( let persist_id: String = engram_node_full(
content, content,
"Conversation", "Conversation",
"chat:" + ts_str, "chat:" + ts_str,
@@ -1152,6 +1176,9 @@ fn auto_persist(req: String, resp: String) -> Void {
"Episodic", "Episodic",
tags tags
) )
if str_eq(persist_id, "") {
println("[chat] auto_persist: engram_node_full returned empty — conversation node lost (ts=" + ts_str + ")")
}
} }
// strengthen_chat_nodes strengthen the engram nodes that were activated during a chat. // strengthen_chat_nodes strengthen the engram nodes that were activated during a chat.
Generated Vendored
+18 -4
View File
@@ -285,10 +285,24 @@ el_val_t proactive_curiosity(void) {
el_val_t wm_top_j = engram_wm_top_json(1); el_val_t wm_top_j = engram_wm_top_json(1);
el_val_t wm_top_n = json_array_get(wm_top_j, 0); el_val_t wm_top_n = json_array_get(wm_top_j, 0);
el_val_t wm_top_lbl = json_get(wm_top_n, EL_STR("label")); el_val_t wm_top_lbl = json_get(wm_top_n, EL_STR("label"));
if (!str_eq(wm_top_lbl, EL_STR(""))) { el_val_t wm_top_type = json_get(wm_top_n, EL_STR("node_type"));
el_val_t sp = str_find_chars(wm_top_lbl, EL_STR(" :([")); state_set(EL_STR("allow_auto"), EL_STR("0"));
if (sp > 3) { if (str_eq(wm_top_type, EL_STR("Memory"))) {
state_set(EL_STR("cseed_auto"), str_slice(wm_top_lbl, 0, sp)); state_set(EL_STR("allow_auto"), EL_STR("1"));
}
if (str_eq(wm_top_type, EL_STR("BacklogItem"))) {
state_set(EL_STR("allow_auto"), EL_STR("1"));
}
if (str_eq(wm_top_type, EL_STR("Entity"))) {
state_set(EL_STR("allow_auto"), EL_STR("1"));
}
el_val_t allow_auto = state_get(EL_STR("allow_auto"));
if (str_eq(allow_auto, EL_STR("1"))) {
if (!str_eq(wm_top_lbl, EL_STR(""))) {
el_val_t sp = str_find_chars(wm_top_lbl, EL_STR(" :(["));
if (sp > 3) {
state_set(EL_STR("cseed_auto"), str_slice(wm_top_lbl, 0, sp));
}
} }
} }
el_val_t auto_term = state_get(EL_STR("cseed_auto")); el_val_t auto_term = state_get(EL_STR("cseed_auto"));
Generated Vendored
+56 -7
View File
@@ -1042,12 +1042,36 @@ el_val_t call_neuron_mcp(el_val_t tool_name, el_val_t args_json);
el_val_t agentic_tools_literal(void); el_val_t agentic_tools_literal(void);
el_val_t agentic_tools_with_web(void); el_val_t agentic_tools_with_web(void);
el_val_t dispatch_tool(el_val_t tool_name, el_val_t tool_input); el_val_t dispatch_tool(el_val_t tool_name, el_val_t tool_input);
el_val_t json_array_append(el_val_t arr, el_val_t item);
el_val_t append_tool_log(el_val_t log, el_val_t name);
el_val_t exec_tool_block(el_val_t block);
el_val_t agentic_blob(el_val_t model, el_val_t system, el_val_t tools_json, el_val_t messages, el_val_t origin, el_val_t approval, el_val_t iteration, el_val_t tools_log, el_val_t content, el_val_t queue, el_val_t results, el_val_t next);
el_val_t extract_all_text(el_val_t s);
el_val_t strip_citations(el_val_t s);
el_val_t agentic_api_turn(el_val_t model, el_val_t safe_sys, el_val_t tools_json, el_val_t messages);
el_val_t agentic_engine(el_val_t session_id, el_val_t blob);
el_val_t handle_chat_agentic(el_val_t body); el_val_t handle_chat_agentic(el_val_t body);
el_val_t handle_session_approve(el_val_t session_id, el_val_t body);
el_val_t handle_chat_as_soul(el_val_t body); el_val_t handle_chat_as_soul(el_val_t body);
el_val_t handle_dharma_room_turn(el_val_t body); el_val_t handle_dharma_room_turn(el_val_t body);
el_val_t handle_dharma_room_turn_agentic(el_val_t body); el_val_t handle_dharma_room_turn_agentic(el_val_t body);
el_val_t auto_persist(el_val_t req, el_val_t resp); el_val_t auto_persist(el_val_t req, el_val_t resp);
el_val_t strengthen_chat_nodes(el_val_t activation_nodes); el_val_t strengthen_chat_nodes(el_val_t activation_nodes);
el_val_t safety_self_harm_phrases(void);
el_val_t safety_abuse_phrases(void);
el_val_t safety_general_hard_phrases(void);
el_val_t safety_soft_phrases(void);
el_val_t safety_normalize(el_val_t message);
el_val_t safety_any_match(el_val_t text, el_val_t phrases_json);
el_val_t safety_count_match(el_val_t text, el_val_t phrases_json);
el_val_t safety_detect_bell_level(el_val_t message);
el_val_t safety_classify_hard_bell(el_val_t message);
el_val_t safety_soft_directive(void);
el_val_t safety_hard_directive(el_val_t hard_type);
el_val_t safety_augment_system(el_val_t system, el_val_t user_msg);
el_val_t safety_contact_path(void);
el_val_t handle_safety_contact_get(void);
el_val_t handle_safety_contact_post(el_val_t body);
el_val_t auth_headers(el_val_t tok); el_val_t auth_headers(el_val_t tok);
el_val_t axon_get(el_val_t path); el_val_t axon_get(el_val_t path);
el_val_t axon_post(el_val_t path, el_val_t body); el_val_t axon_post(el_val_t path, el_val_t body);
@@ -1110,6 +1134,7 @@ el_val_t session_update_meta_timestamp(el_val_t session_id);
el_val_t session_auto_title(el_val_t session_id, el_val_t first_message); el_val_t session_auto_title(el_val_t session_id, el_val_t first_message);
el_val_t handle_session_approve(el_val_t session_id, el_val_t body); el_val_t handle_session_approve(el_val_t session_id, el_val_t body);
el_val_t strip_query(el_val_t path); el_val_t strip_query(el_val_t path);
el_val_t flag_true(el_val_t body, el_val_t key);
el_val_t err_404(el_val_t path); el_val_t err_404(el_val_t path);
el_val_t err_405(el_val_t method, el_val_t path); el_val_t err_405(el_val_t method, el_val_t path);
el_val_t route_health(void); el_val_t route_health(void);
@@ -1144,6 +1169,9 @@ el_val_t local_node_count;
el_val_t snapshot_usable; el_val_t snapshot_usable;
el_val_t boot_num; el_val_t boot_num;
el_val_t is_genesis; el_val_t is_genesis;
el_val_t guard_disk;
el_val_t guard_disk_len;
el_val_t safe_to_seed;
el_val_t lang_profile(el_val_t code, el_val_t word_order, el_val_t morph_type, el_val_t has_case, el_val_t has_gender, el_val_t script_dir, el_val_t agreement, el_val_t null_subject) { el_val_t lang_profile(el_val_t code, el_val_t word_order, el_val_t morph_type, el_val_t has_case, el_val_t has_gender, el_val_t script_dir, el_val_t agreement, el_val_t null_subject) {
el_val_t r = native_list_empty(); el_val_t r = native_list_empty();
@@ -25890,14 +25918,28 @@ el_val_t proactive_curiosity(void) {
el_val_t wm_top_j = engram_wm_top_json(1); el_val_t wm_top_j = engram_wm_top_json(1);
el_val_t wm_top_n = json_array_get(wm_top_j, 0); el_val_t wm_top_n = json_array_get(wm_top_j, 0);
el_val_t wm_top_lbl = json_get(wm_top_n, EL_STR("label")); el_val_t wm_top_lbl = json_get(wm_top_n, EL_STR("label"));
if (!str_eq(wm_top_lbl, EL_STR(""))) { el_val_t wm_top_type = json_get(wm_top_n, EL_STR("node_type"));
el_val_t sp = str_find_chars(wm_top_lbl, EL_STR(" :([")); state_set(EL_STR("allow_auto"), EL_STR("0"));
if (sp > 3) { if (str_eq(wm_top_type, EL_STR("Memory"))) {
state_set(EL_STR("cseed_auto"), str_slice(wm_top_lbl, 0, sp)); state_set(EL_STR("allow_auto"), EL_STR("1"));
}
if (str_eq(wm_top_type, EL_STR("BacklogItem"))) {
state_set(EL_STR("allow_auto"), EL_STR("1"));
}
if (str_eq(wm_top_type, EL_STR("Entity"))) {
state_set(EL_STR("allow_auto"), EL_STR("1"));
}
el_val_t allow_auto = state_get(EL_STR("allow_auto"));
if (str_eq(allow_auto, EL_STR("1"))) {
if (!str_eq(wm_top_lbl, EL_STR(""))) {
el_val_t sp = str_find_chars(wm_top_lbl, EL_STR(" :(["));
if (sp > 3) {
state_set(EL_STR("cseed_auto"), str_slice(wm_top_lbl, 0, sp));
}
} }
} }
el_val_t auto_term = state_get(EL_STR("cseed_auto")); el_val_t auto_term = state_get(EL_STR("cseed_auto"));
el_val_t results_auto = ({ el_val_t _if_result_101 = 0; if (str_eq(auto_term, EL_STR(""))) { _if_result_101 = (EL_STR("[]")); } else { _if_result_101 = (engram_activate_json(auto_term, 1)); } _if_result_101; }); el_val_t results_auto = ({ el_val_t _if_result_3 = 0; if (str_eq(auto_term, EL_STR(""))) { _if_result_3 = (EL_STR("[]")); } else { _if_result_3 = (engram_activate_json(auto_term, 1)); } _if_result_3; });
el_val_t found_auto = json_array_len(results_auto); el_val_t found_auto = json_array_len(results_auto);
el_val_t total_found = (found + found_auto); el_val_t total_found = (found + found_auto);
el_val_t safe_auto = str_replace(auto_term, EL_STR("\""), EL_STR("'")); el_val_t safe_auto = str_replace(auto_term, EL_STR("\""), EL_STR("'"));
@@ -25908,6 +25950,7 @@ el_val_t proactive_curiosity(void) {
return 0; return 0;
} }
el_val_t pulse_count(void) { el_val_t pulse_count(void) {
el_val_t s = state_get(EL_STR("soul.pulse")); el_val_t s = state_get(EL_STR("soul.pulse"));
if (str_eq(s, EL_STR(""))) { if (str_eq(s, EL_STR(""))) {
@@ -28915,7 +28958,13 @@ int main(int _argc, char** _argv) {
state_set(EL_STR("soul_engram_api_key"), engram_api_key_raw); state_set(EL_STR("soul_engram_api_key"), engram_api_key_raw);
state_set(EL_STR("soul.running"), EL_STR("true")); state_set(EL_STR("soul.running"), EL_STR("true"));
is_genesis = str_eq(soul_cgi_id, EL_STR("ntn-genesis")); is_genesis = str_eq(soul_cgi_id, EL_STR("ntn-genesis"));
if (is_genesis) { guard_disk = ({ el_val_t _if_result_25 = 0; if (str_eq(engram_url_raw, EL_STR(""))) { _if_result_25 = (fs_read(snapshot)); } else { _if_result_25 = (EL_STR("")); } _if_result_25; });
guard_disk_len = str_len(guard_disk);
safe_to_seed = !((guard_disk_len > 200000) && (engram_node_count() < (guard_disk_len / 16000)));
if (is_genesis && !safe_to_seed) {
println(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("[soul] GUARD: loaded "), int_to_str(engram_node_count())), EL_STR(" nodes but snapshot file is ")), int_to_str(guard_disk_len)), EL_STR(" bytes \xe2\x80\x94 refusing to seed/save over a real graph")));
}
if (is_genesis && safe_to_seed) {
el_val_t edge_count_now = engram_edge_count(); el_val_t edge_count_now = engram_edge_count();
if (edge_count_now < 100) { if (edge_count_now < 100) {
init_soul_edges(); init_soul_edges();
@@ -28926,7 +28975,7 @@ int main(int _argc, char** _argv) {
state_set(EL_STR("soul_snapshot_path"), snapshot); state_set(EL_STR("soul_snapshot_path"), snapshot);
engram_save(snapshot); engram_save(snapshot);
} }
if (is_genesis) { if (is_genesis && safe_to_seed) {
el_val_t snap = state_get(EL_STR("soul_snapshot_path")); el_val_t snap = state_get(EL_STR("soul_snapshot_path"));
if (!str_eq(snap, EL_STR(""))) { if (!str_eq(snap, EL_STR(""))) {
engram_save(snap); engram_save(snap);
+8 -2
View File
@@ -46,7 +46,10 @@ fn mem_consolidate() -> String {
} }
fn mem_save(path: String) -> Void { fn mem_save(path: String) -> Void {
engram_save(path) let save_result: String = engram_save(path)
if str_eq(save_result, "") {
println("[memory] mem_save: engram_save failed for " + path + " — snapshot may be incomplete")
}
} }
fn mem_load(path: String) -> Void { fn mem_load(path: String) -> Void {
@@ -76,11 +79,14 @@ fn mem_boot_count_inc() -> Int {
let next: Int = current + 1 let next: Int = current + 1
let content: String = "soul:boot_count:" + int_to_str(next) let content: String = "soul:boot_count:" + int_to_str(next)
let tags: String = "[\"soul-meta\",\"boot-counter\"]" let tags: String = "[\"soul-meta\",\"boot-counter\"]"
let discard: String = engram_node_full( let boot_node_id: String = engram_node_full(
content, "Memory", "soul:boot_count", content, "Memory", "soul:boot_count",
el_from_float(0.9), el_from_float(0.9), el_from_float(1.0), el_from_float(0.9), el_from_float(0.9), el_from_float(1.0),
"Canonical", tags "Canonical", tags
) )
if str_eq(boot_node_id, "") {
println("[memory] mem_boot_count_inc: engram write failed — boot counter node lost (count=" + int_to_str(next) + ")")
}
return next return next
} }
+10 -2
View File
@@ -400,6 +400,7 @@ fn handle_api_log_state_event(body: String) -> String {
let id: String = engram_node_full(parts, "InternalStateEvent", "state-event:manual", let id: String = engram_node_full(parts, "InternalStateEvent", "state-event:manual",
el_from_float(0.85), el_from_float(0.85), el_from_float(0.9), el_from_float(0.85), el_from_float(0.85), el_from_float(0.9),
"Episodic", tags) "Episodic", tags)
if !api_persisted(id) { return api_not_persisted(id) }
return "{\"ok\":true,\"id\":\"" + id + "\",\"boot\":\"" + boot + "\"}" return "{\"ok\":true,\"id\":\"" + id + "\",\"boot\":\"" + boot + "\"}"
} }
@@ -452,6 +453,7 @@ fn handle_api_tune_config(body: String) -> String {
let id: String = engram_node_full(content, "ConfigEntry", key, let id: String = engram_node_full(content, "ConfigEntry", key,
el_from_float(0.85), el_from_float(0.85), el_from_float(0.9), el_from_float(0.85), el_from_float(0.85), el_from_float(0.9),
"Canonical", tags) "Canonical", tags)
if !api_persisted(id) { return api_not_persisted(id) }
return "{\"ok\":true,\"key\":\"" + key + "\",\"value\":\"" + value + "\",\"id\":\"" + id + "\"}" return "{\"ok\":true,\"key\":\"" + key + "\",\"value\":\"" + value + "\",\"id\":\"" + id + "\"}"
} }
@@ -651,17 +653,23 @@ fn handle_api_consolidate(body: String) -> String {
let summary: String = json_get(body, "summary") let summary: String = json_get(body, "summary")
let snap: String = state_get("soul_snapshot_path") let snap: String = state_get("soul_snapshot_path")
if !str_eq(snap, "") { if !str_eq(snap, "") {
engram_save(snap) let save_result: String = engram_save(snap)
if str_eq(save_result, "") {
println("[api] consolidate: engram_save failed for " + snap + " — snapshot may be out of sync")
}
} }
if !str_eq(summary, "") { if !str_eq(summary, "") {
let safe_summary: String = str_replace(summary, "\"", "'") let safe_summary: String = str_replace(summary, "\"", "'")
let tags: String = "[\"SessionSummary\",\"consolidate\"]" let tags: String = "[\"SessionSummary\",\"consolidate\"]"
let discard: String = engram_node_full( let summary_id: String = engram_node_full(
"[session-summary] " + safe_summary, "[session-summary] " + safe_summary,
"SessionSummary", "session:summary", "SessionSummary", "session:summary",
el_from_float(0.7), el_from_float(0.7), el_from_float(0.9), el_from_float(0.7), el_from_float(0.7), el_from_float(0.9),
"Episodic", tags "Episodic", tags
) )
if str_eq(summary_id, "") {
println("[api] consolidate: session summary engram write failed — summary node lost")
}
} }
return "{\"ok\":true,\"snapshot\":\"" + snap + "\"}" return "{\"ok\":true,\"snapshot\":\"" + snap + "\"}"
} }
+10 -2
View File
@@ -212,8 +212,13 @@ fn seed_persona_from_env() -> Void {
let h: Map = {} let h: Map = {}
map_set(h, "Content-Type", "application/json") map_set(h, "Content-Type", "application/json")
let resp: String = http_post_with_headers(engram_url + "/api/nodes", body, h) let resp: String = http_post_with_headers(engram_url + "/api/nodes", body, h)
if str_contains(resp, "\"error\"") { // Check for empty response (timeout/network error), explicit error, or missing id.
if str_eq(resp, "") {
println("[soul] persona HTTP write-back failed: empty response (timeout or network error) — in-memory only this session")
} else if str_contains(resp, "\"error\"") {
println("[soul] persona HTTP write-back failed (in-memory only this session): " + resp) println("[soul] persona HTTP write-back failed (in-memory only this session): " + resp)
} else if !str_contains(resp, "\"id\"") {
println("[soul] persona HTTP write-back: unexpected response (no id field) — in-memory only this session: " + resp)
} else { } else {
println("[soul] persona persisted to HTTP engram at " + engram_url) println("[soul] persona persisted to HTTP engram at " + engram_url)
} }
@@ -246,11 +251,14 @@ fn emit_session_start_event() -> Void {
+ ",\"ts\":" + int_to_str(ts) + "}" + ",\"ts\":" + int_to_str(ts) + "}"
let tags: String = "[\"internal-state\",\"session-start\",\"InternalStateEvent\"]" let tags: String = "[\"internal-state\",\"session-start\",\"InternalStateEvent\"]"
let discard: String = engram_node_full( let session_event_id: String = engram_node_full(
payload, "InternalStateEvent", "session-start", payload, "InternalStateEvent", "session-start",
el_from_float(0.9), el_from_float(0.9), el_from_float(1.0), el_from_float(0.9), el_from_float(0.9), el_from_float(1.0),
"Episodic", tags "Episodic", tags
) )
if str_eq(session_event_id, "") {
println("[soul] emit_session_start_event: engram write failed — session-start event lost")
}
println("[soul] session-start event logged (boot=" + boot_num + " nodes=" + int_to_str(node_ct) + " edges=" + int_to_str(edge_ct) + ")") println("[soul] session-start event logged (boot=" + boot_num + " nodes=" + int_to_str(node_ct) + " edges=" + int_to_str(edge_ct) + ")")
} }