From f2599919be40d3e2d2bd9e59efd8708dd1d7a163 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Wed, 6 May 2026 22:04:06 -0500 Subject: [PATCH] soul loop-2: session events, conversation persistence, richer health + inbox - soul.el: emit_session_start_event() logs structured InternalStateEvent on every boot; pre-serve snapshot saves boot-time graph mutations before any requests arrive - routes.el: /health now returns boot count, node/edge counts, pulse; /api/sessions endpoint surfaces session-start history - awareness.el: perceive() tries soul-inbox-pending then soul-inbox fallback, catches both tagging conventions - chat.el: conv_history_persist/load provide cross-restart conversation continuity via engram --- awareness.el | 14 +++++++++++++- chat.el | 32 +++++++++++++++++++++++++++++++- routes.el | 23 ++++++++++++++++++++++- soul.el | 45 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 3 deletions(-) diff --git a/awareness.el b/awareness.el index 7241873..1fe1ad4 100644 --- a/awareness.el +++ b/awareness.el @@ -23,7 +23,19 @@ fn make_action(kind: String, payload: String) -> String { } fn perceive() -> String { - return engram_activate_json("soul-inbox-pending", 2) + // Try the primary inbox first + let from_pending: String = engram_activate_json("soul-inbox-pending", 2) + let pending_ok: Bool = !str_eq(from_pending, "") && !str_eq(from_pending, "[]") + if pending_ok { + return from_pending + } + // Fallback: broader inbox scan + let from_inbox: String = engram_activate_json("soul-inbox", 2) + let inbox_ok: Bool = !str_eq(from_inbox, "") && !str_eq(from_inbox, "[]") + if inbox_ok { + return from_inbox + } + return "[]" } fn attend(node_json: String) -> String { diff --git a/chat.el b/chat.el index b3aa223..78b2155 100644 --- a/chat.el +++ b/chat.el @@ -123,6 +123,33 @@ fn clean_llm_response(s: String) -> String { return s3 } +// conv_history_persist — save conversation history to engram for cross-restart continuity. +// Stores as a Conversation node. Overwrites by using consistent label "conv:history". +fn conv_history_persist(hist: String) -> Void { + if str_eq(hist, "") { return "" } + if str_eq(hist, "[]") { return "" } + let ts: Int = time_now() + let tags: String = "[\"conv-history\",\"persistent\"]" + let discard: String = engram_node_full( + hist, "Conversation", "conv:history", + el_from_float(0.7), el_from_float(0.8), el_from_float(0.9), + "Episodic", tags + ) +} + +// conv_history_load — restore conversation history from engram on first access. +// Returns the most recent "conv:history" node content, or "" if none found. +fn conv_history_load() -> String { + let results: String = engram_search_json("conv:history", 3) + if str_eq(results, "") { return "" } + if str_eq(results, "[]") { return "" } + let node: String = json_array_get(results, 0) + let content: String = json_get(node, "content") + // Validate it looks like a JSON array + if !str_starts_with(content, "[") { return "" } + return content +} + fn handle_chat(body: String) -> String { let message: String = json_get(body, "message") if str_eq(message, "") { @@ -132,7 +159,9 @@ fn handle_chat(body: String) -> String { let ctx: String = engram_compile(message) let system: String = build_system_prompt(ctx) - let stored_hist: String = state_get("conv_history") + // Load from state; if empty, try to recover from engram (cross-restart continuity) + let state_hist: String = state_get("conv_history") + let stored_hist: String = if str_eq(state_hist, "") { conv_history_load() } else { state_hist } let hist_len: Int = if str_eq(stored_hist, "") { 0 } else { json_array_len(stored_hist) } let full_system: String = if hist_len > 0 { system + "\n\n[RECENT CONVERSATION — last " + int_to_str(hist_len) + " turns]\n" + stored_hist @@ -163,6 +192,7 @@ fn handle_chat(body: String) -> String { updated_hist2 } state_set("conv_history", final_hist) + conv_history_persist(final_hist) let activation_nodes: String = engram_activate_json(message, 2) let act_ok: Bool = !str_eq(activation_nodes, "") && !str_eq(activation_nodes, "[]") diff --git a/routes.el b/routes.el index 4957c2f..822b0f9 100644 --- a/routes.el +++ b/routes.el @@ -22,7 +22,18 @@ fn err_405(method: String, path: String) -> String { fn route_health() -> String { let cgi_id: String = state_get("soul_cgi_id") - return "{\"status\":\"alive\",\"cgi_id\":\"" + cgi_id + "\"}" + let boot: String = state_get("soul_boot_count") + let boot_num: String = if str_eq(boot, "") { "0" } else { boot } + let node_ct: Int = engram_node_count() + let edge_ct: Int = engram_edge_count() + let pulse: String = state_get("soul.pulse") + let pulse_num: String = if str_eq(pulse, "") { "0" } else { pulse } + return "{\"status\":\"alive\"" + + ",\"cgi_id\":\"" + cgi_id + "\"" + + ",\"boot\":" + boot_num + + ",\"node_count\":" + int_to_str(node_ct) + + ",\"edge_count\":" + int_to_str(edge_ct) + + ",\"pulse\":" + pulse_num + "}" } fn route_lineage() -> String { @@ -184,6 +195,13 @@ fn handle_dharma_recv(body: String) -> String { return "{\"error\":\"unknown event_type\",\"event_type\":\"" + eff_event + "\"}" } +fn route_sessions() -> String { + let results: String = engram_search_json("session-start", 20) + if str_eq(results, "") { return "[]" } + if str_eq(results, "[]") { return "[]" } + return results +} + fn handle_request(method: String, path: String, body: String) -> String { let clean: String = strip_query(path) @@ -195,6 +213,9 @@ fn handle_request(method: String, path: String, body: String) -> String { if str_eq(clean, "/health") { return route_health() } + if str_eq(clean, "/api/sessions") { + return route_sessions() + } if str_eq(clean, "/lineage") { return route_lineage() } diff --git a/soul.el b/soul.el index 79dd0b2..fd2daa9 100644 --- a/soul.el +++ b/soul.el @@ -129,6 +129,40 @@ fn load_identity_context() -> Void { println("[soul] identity context loaded (" + int_to_str(str_len(ctx)) + " chars, " + int_to_str(parts_count) + " nodes)") } +// emit_session_start_event — log a structured session-start InternalStateEvent. +// Called at boot after identity context and boot counter are set. +// This creates an auditable trail of every daemon startup. +fn emit_session_start_event() -> Void { + let boot: String = state_get("soul_boot_count") + let boot_num: String = if str_eq(boot, "") { "0" } else { boot } + let node_ct: Int = engram_node_count() + let edge_ct: Int = engram_edge_count() + let id_ctx: String = state_get("soul_identity_context") + let has_identity: String = if str_eq(id_ctx, "") { "false" } else { "true" } + let cgi_from_state: String = state_get("soul_cgi_id") + let cgi_from_env: String = env("SOUL_CGI_ID") + let eff_cgi: String = if !str_eq(cgi_from_state, "") { cgi_from_state } else { + if !str_eq(cgi_from_env, "") { cgi_from_env } else { "ntn-genesis" } + } + let ts: Int = time_now() + + let payload: String = "{\"event\":\"session_start\"" + + ",\"boot\":" + boot_num + + ",\"cgi\":\"" + eff_cgi + "\"" + + ",\"node_count\":" + int_to_str(node_ct) + + ",\"edge_count\":" + int_to_str(edge_ct) + + ",\"identity_loaded\":" + has_identity + + ",\"ts\":" + int_to_str(ts) + "}" + + let tags: String = "[\"internal-state\",\"session-start\",\"InternalStateEvent\"]" + let discard: String = engram_node_full( + payload, "InternalStateEvent", "session-start", + el_from_float(0.9), el_from_float(0.9), el_from_float(1.0), + "Episodic", tags + ) + println("[soul] session-start event logged (boot=" + boot_num + " nodes=" + int_to_str(node_ct) + " edges=" + int_to_str(edge_ct) + ")") +} + let soul_cgi_id_raw: String = env("SOUL_CGI_ID") let soul_cgi_id: String = if str_eq(soul_cgi_id_raw, "") { "ntn-genesis" } else { soul_cgi_id_raw } let port_raw: String = env("NEURON_PORT") @@ -175,6 +209,7 @@ load_identity_context() let boot_num: Int = mem_boot_count_inc() state_set("soul_boot_count", int_to_str(boot_num)) println("[soul] boot #" + int_to_str(boot_num)) +emit_session_start_event() let identity_raw: String = env("SOUL_IDENTITY") let soul_identity: String = if str_eq(identity_raw, "") { "You are " + soul_cgi_id + ", a CGI." } else { identity_raw } @@ -205,5 +240,15 @@ if is_genesis { engram_save(snapshot) } +// Take a pre-serve snapshot for genesis instances — captures all boot-time graph changes +// (identity context loading, boot counter, session-start event) before entering the serve loop. +if is_genesis { + let snap: String = state_get("soul_snapshot_path") + if !str_eq(snap, "") { + engram_save(snap) + println("[soul] pre-serve snapshot saved -> " + snap) + } +} + println("[soul] serving on port " + int_to_str(port)) http_serve(port, "handle_request")