diff --git a/soul.el b/soul.el index c58b03d..ab1809a 100644 --- a/soul.el +++ b/soul.el @@ -162,6 +162,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. @@ -254,6 +287,9 @@ 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 { + // 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")