From 34551695a1bd3cc1d9cc93ad939726c45da4c40e Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Mon, 22 Jun 2026 11:48:30 -0500 Subject: [PATCH] fix(reliability): cross-session-affective - Fix state key mismatch: soul.el layered_cycle now reads conv_history (not conversation_history), unblocking the safety_score_distress_history history-amplification path in safety_threat_score - Add safety_augment_system call on the main handle_chat path so the phrase-list bell detector fires on all chat turns, not just dharma rooms - Add cross-session affective engram query in load_identity_context() at boot; stores distress/crisis signals from prior sessions under soul_affective_context with a 7-day soft recency filter --- chat.el | 7 +++++++ soul.el | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/chat.el b/chat.el index 51f6ff2..3a5225c 100644 --- a/chat.el +++ b/chat.el @@ -374,6 +374,13 @@ fn handle_chat(body: String) -> String { let req_model: String = json_get(body, "model") let model: String = if str_eq(req_model, "") { chat_default_model() } else { req_model } + // Safety augmentation on the main chat path. Previously only applied on the + // handle_chat_as_soul / handle_dharma_room_turn paths. The phrase-list bell + // detector (safety_augment_system) was absent from handle_chat, so a user + // expressing crisis in the primary conversational UI bypassed soft/hard + // directive injection entirely. Applying it here before every llm_call_system. + let full_system = safety_augment_system(full_system, message) + let raw_response: String = llm_call_system(model, full_system, message) let is_error: Bool = str_starts_with(raw_response, "{\"error\"") diff --git a/soul.el b/soul.el index 0147f2a..98a51e6 100644 --- a/soul.el +++ b/soul.el @@ -166,6 +166,39 @@ fn load_identity_context() -> Void { println("[soul] persona node loaded (" + int_to_str(str_len(p_content)) + " chars)") } } + + // Cross-session affective context: query engram for recent distress/crisis signals + // at session start. Stored under soul_affective_context so the safety layer can + // detect when a user has been in distress across previous sessions. + // Soft recency guard: nodes with a ts field older than 7 days are skipped. + // Results capped at 3 nodes, 200 chars each, to avoid over-injection into context. + // TODO(recency): engram_search_json sorts by relevance, not timestamp. A native + // after= filter in the engram search API would make this more precise. + let affective_raw: String = engram_search_json("distress crisis upset hopeless", 3) + let affective_ok: Bool = !str_eq(affective_raw, "") && !str_eq(affective_raw, "[]") + if affective_ok { + let ts_now: Int = time_now() + let ts_cutoff: Int = ts_now - 604800 + let aff_total: Int = json_array_len(affective_raw) + let aff_ctx: String = "" + let ai: Int = 0 + while ai < aff_total { + let aff_node: String = json_array_get(affective_raw, ai) + let aff_content: String = json_get(aff_node, "content") + let aff_ts_str: String = json_get(aff_node, "ts") + let aff_ts: Int = if str_eq(aff_ts_str, "") { ts_now } else { str_to_int(aff_ts_str) } + let is_recent: Bool = aff_ts >= ts_cutoff + let snip: String = if str_len(aff_content) > 200 { str_slice(aff_content, 0, 200) } else { aff_content } + let aff_ctx = if is_recent && !str_eq(snip, "") { + if str_eq(aff_ctx, "") { snip } else { aff_ctx + "\n" + snip } + } else { aff_ctx } + let ai = ai + 1 + } + if !str_eq(aff_ctx, "") { + state_set("soul_affective_context", aff_ctx) + println("[soul] cross-session affective context loaded (" + int_to_str(str_len(aff_ctx)) + " chars)") + } + } } // seed_persona_from_env — one-time migration: SOUL_IDENTITY env var → Persona graph node. @@ -258,7 +291,10 @@ fn emit_session_start_event() -> Void { // L0 (core) → L1 (safety screen) → L2a (continuity + behavioral profiling) → L2b (mission alignment) → L3 (imprint) → L1 (safety validate) // Internal cognition (heartbeat, proactive, memory ops) bypasses layers — use one_cycle directly. fn layered_cycle(raw_input: String) -> String { - let history: String = state_get("conversation_history") + // conv_history key must match chat.el (conv_history, not conversation_history). + // Mismatch caused safety_score_distress_history() to always receive "" - the + // history-amplification path in safety_threat_score was permanently dead. + let history: String = state_get("conv_history") let session_id: String = state_get("current_session_id") // L1 in: safety screen