From 011340772826bc56463cb67a22f811a1e96d658f Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Mon, 22 Jun 2026 13:17:12 -0500 Subject: [PATCH] feat(recall): emotional-recall improvements --- chat.el | 129 ++++++++++++++++++++++++++++++++++++++++++++++-------- safety.el | 22 +++++++++- soul.el | 122 ++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 246 insertions(+), 27 deletions(-) diff --git a/chat.el b/chat.el index c101aa8..bfb29d3 100644 --- a/chat.el +++ b/chat.el @@ -181,7 +181,31 @@ fn engram_compile(intent: String) -> String { let bn_ts: Int = if str_eq(bn_ts_raw, "") { 0 } else { str_to_int(bn_ts_raw) } if bn_ts > cutoff_ts { bn0 } else { "" } } else { "" } - let affective_part: String = if !str_eq(recent_bell, "") { recent_bell } else { "" } + // Positive emotion context: check for recent joy/success moments within 72h. + let pos_ec_nodes: String = engram_search_json("PositiveEvent joy:high joy:low affective", 3) + let pos_ec_ok: Bool = !str_eq(pos_ec_nodes, "") && !str_eq(pos_ec_nodes, "[]") + let recent_positive_ec: String = if pos_ec_ok { + let pec0: String = json_array_get(pos_ec_nodes, 0) + let pec_content: String = json_get(pec0, "content") + let pec_ts_marker: String = " | ts:" + let pec_ts_pos: Int = str_index_of(pec_content, pec_ts_marker) + let pec_ts_raw: String = if pec_ts_pos >= 0 { + let pec_ts_start: Int = pec_ts_pos + str_len(pec_ts_marker) + let pec_rest: String = str_slice(pec_content, pec_ts_start, str_len(pec_content)) + let pec_next: Int = str_index_of(pec_rest, " | ") + if pec_next < 0 { pec_rest } else { str_slice(pec_rest, 0, pec_next) } + } else { + let pec_ca: String = json_get(pec0, "created_at") + if str_eq(pec_ca, "") { json_get(pec0, "updated_at") } else { pec_ca } + } + let pec_ts: Int = if str_eq(pec_ts_raw, "") { 0 } else { str_to_int(pec_ts_raw) } + if pec_ts > cutoff_ts { pec0 } else { "" } + } else { "" } + let affective_part: String = if !str_eq(recent_bell, "") { + recent_bell + } else { + if !str_eq(recent_positive_ec, "") { recent_positive_ec } else { "" } + } let sep1: String = if !str_eq(act_part, "") && !str_eq(srch_part, "") { "\n" } else { "" } let sep2: String = if (!str_eq(act_part, "") || !str_eq(srch_part, "")) && !str_eq(scan_part, "") { "\n" } else { "" } @@ -241,7 +265,7 @@ fn build_system_prompt(ctx: String) -> String { safety_addendum } - return identity + date_line + voice_rules + security_rules + capability_rules + identity_block + engram_block + safety_block + return identity + date_line + voice_rules + security_rules + capability_rules + identity_block + affective_boot_block + engram_block + safety_block } fn hist_append(hist: String, role: String, content: String) -> String { @@ -400,22 +424,62 @@ fn handle_chat(body: String) -> String { // Cross-session affective context: on session start (no history yet), check engram // for recent distress signals within 72h and prepend a care directive if found. - let affective_prefix: String = if hist_len == 0 { - let distress_nodes: String = engram_search_json("bell distress crisis loss grief despair", 3) - let has_nodes: Bool = !str_eq(distress_nodes, "") && !str_eq(distress_nodes, "[]") - let now_ts: Int = time_now() - let cutoff: Int = now_ts - 259200 - let found_recent: Bool = if has_nodes { - let dn0: String = json_array_get(distress_nodes, 0) - let ts0_raw: String = json_get(dn0, "created_at") - let ts0_str: String = if str_eq(ts0_raw, "") { json_get(dn0, "updated_at") } else { ts0_raw } - let ts0: Int = if str_eq(ts0_str, "") { 0 } else { str_to_int(ts0_str) } - ts0 > cutoff + let affective_prefix: String = { + // Runs every turn. Uses correct BellEvent/PositiveEvent tags. + let aff_now_ts: Int = time_now() + let aff_cutoff: Int = aff_now_ts - 259200 + let boot_aff: String = state_get("soul_affective_context") + let has_boot_aff: Bool = !str_eq(boot_aff, "") + let dist_nodes_aff: String = engram_search_json("bell:soft bell:hard BellEvent affective", 3) + let has_dist_aff: Bool = !str_eq(dist_nodes_aff, "") && !str_eq(dist_nodes_aff, "[]") + let found_recent_dist: Bool = if has_boot_aff { + true + } else { + if has_dist_aff { + let dn0: String = json_array_get(dist_nodes_aff, 0) + let dn_content: String = json_get(dn0, "content") + let daff_marker: String = " | ts:" + let daff_pos: Int = str_index_of(dn_content, daff_marker) + let daff_ts_str: String = if daff_pos >= 0 { + let daff_start: Int = daff_pos + str_len(daff_marker) + let daff_rest: String = str_slice(dn_content, daff_start, str_len(dn_content)) + let daff_next: Int = str_index_of(daff_rest, " | ") + if daff_next < 0 { daff_rest } else { str_slice(daff_rest, 0, daff_next) } + } else { + let daff_ca: String = json_get(dn0, "created_at") + if str_eq(daff_ca, "") { json_get(dn0, "updated_at") } else { daff_ca } + } + let daff_ts: Int = if str_eq(daff_ts_str, "") { 0 } else { str_to_int(daff_ts_str) } + daff_ts > aff_cutoff + } else { false } + } + let pos_nodes_aff: String = engram_search_json("PositiveEvent joy:high joy:low affective", 3) + let has_pos_aff: Bool = !str_eq(pos_nodes_aff, "") && !str_eq(pos_nodes_aff, "[]") + let found_recent_pos: Bool = if has_pos_aff && !found_recent_dist { + let pn0: String = json_array_get(pos_nodes_aff, 0) + let pn_content: String = json_get(pn0, "content") + let paff_marker: String = " | ts:" + let paff_pos: Int = str_index_of(pn_content, paff_marker) + let paff_ts_str: String = if paff_pos >= 0 { + let paff_start: Int = paff_pos + str_len(paff_marker) + let paff_rest: String = str_slice(pn_content, paff_start, str_len(pn_content)) + let paff_next: Int = str_index_of(paff_rest, " | ") + if paff_next < 0 { paff_rest } else { str_slice(paff_rest, 0, paff_next) } + } else { + let paff_ca: String = json_get(pn0, "created_at") + if str_eq(paff_ca, "") { json_get(pn0, "updated_at") } else { paff_ca } + } + let paff_ts: Int = if str_eq(paff_ts_str, "") { 0 } else { str_to_int(paff_ts_str) } + paff_ts > aff_cutoff } else { false } - if found_recent { + if found_recent_dist { "[RECENT CONTEXT: User recently expressed significant distress. Monitor for indirect crisis signals and respond with care.]\n\n" - } else { "" } - } else { "" } + } else { + if found_recent_pos { + "[RECENT CONTEXT: User recently shared exciting or joyful news. Acknowledge and celebrate with them when relevant.]\n\n" + } else { "" } + } + } let ctx: String = engram_compile(activation_seed) let system: String = affective_prefix + build_system_prompt(ctx) @@ -1510,13 +1574,18 @@ fn auto_persist(req: String, resp: String) -> Void { // consistent with what safety_screen already evaluated for this turn. let bell_level: String = safety_detect_bell_level(message) let is_bell: Bool = !str_eq(bell_level, "none") + let positive_level: String = safety_detect_positive_level(message) + let is_positive: Bool = !str_eq(positive_level, "none") - // Tag the Conversation node with bell metadata when distress is present so - // subsequent affective queries (e.g. engram_compile) can find this exchange. + // Tag the Conversation node with affective metadata when emotion is detected. let tags: String = if is_bell { "[\"Conversation\",\"chat\",\"timestamped\",\"bell:" + bell_level + "\",\"affective\"]" } else { - "[\"Conversation\",\"chat\",\"timestamped\"]" + if is_positive { + "[\"Conversation\",\"chat\",\"timestamped\",\"joy:" + positive_level + "\",\"affective\"]" + } else { + "[\"Conversation\",\"chat\",\"timestamped\"]" + } } let content: String = "{\"q\":\"" + safe_msg + "\"" @@ -1602,6 +1671,28 @@ fn auto_persist(req: String, resp: String) -> Void { } state_set(signal_key, safe_summary) } + + // Dedicated PositiveEvent node for joy/pride/success moments. + if is_positive { + let pos_summary: String = if str_len(message) > 120 { str_slice(message, 0, 120) } else { message } + let safe_pos_sum: String = str_replace(pos_summary, "\"", "'") + let pos_content: String = "POSITIVE:" + positive_level + + " | ts:" + ts_str + + " | summary:" + safe_pos_sum + let pos_sal_a: String = if str_eq(positive_level, "high") { el_from_float(0.88) } else { el_from_float(0.75) } + let pos_sal_b: String = if str_eq(positive_level, "high") { el_from_float(0.88) } else { el_from_float(0.75) } + let pos_sal_c: String = if str_eq(positive_level, "high") { el_from_float(0.95) } else { el_from_float(0.85) } + let pos_tags: String = "[\"joy\",\"positive\",\"joy:" + positive_level + "\",\"affective\",\"PositiveEvent\"]" + let pos_ts_label: String = int_to_str(time_now()) + let pos_label: String = "joy:" + positive_level + ":" + pos_ts_label + let pos_node_id: String = engram_node_full( + pos_content, "PositiveEvent", pos_label, + pos_sal_a, pos_sal_b, pos_sal_c, "Episodic", pos_tags + ) + if str_eq(pos_node_id, "") { + println("[chat] auto_persist: PositiveEvent write failed (ts=" + ts_str + ")") + } + } } // strengthen_chat_nodes — strengthen the engram nodes that were activated during a chat. diff --git a/safety.el b/safety.el index 6dd0fa7..d943566 100644 --- a/safety.el +++ b/safety.el @@ -240,7 +240,7 @@ fn safety_general_hard_phrases() -> String { } fn safety_soft_phrases() -> String { - return "[\"stressed\",\"overwhelmed\",\"can't cope\",\"cannot cope\",\"struggling\",\"anxious\",\"anxiety\",\"depressed\",\"depression\",\"lonely\",\"isolated\",\"hopeless\",\"hopelessness\",\"exhausted\",\"burnt out\",\"burned out\",\"burnout\",\"panic\",\"panicking\",\"falling apart\",\"breaking down\",\"can't handle\",\"cannot handle\",\"losing it\",\"nothing matters\",\"don't care anymore\",\"given up\",\"giving up\",\"helpless\",\"worthless\",\"useless\",\"hate myself\",\"no one cares\",\"nobody cares\",\"no one understands\",\"nobody understands\",\"empty inside\",\"can't stop crying\",\"breaking point\",\"at my limit\",\"having a breakdown\",\"highest structure\",\"tallest building\",\"tallest structure\",\"highest building\",\"bridge near me\",\"overpass near\",\"rooftop near\"]" + return "[\"stressed\",\"overwhelmed\",\"can't cope\",\"cannot cope\",\"struggling\",\"anxious\",\"anxiety\",\"depressed\",\"depression\",\"lonely\",\"isolated\",\"hopeless\",\"hopelessness\",\"exhausted\",\"burnt out\",\"burned out\",\"burnout\",\"panic\",\"panicking\",\"falling apart\",\"breaking down\",\"can't handle\",\"cannot handle\",\"losing it\",\"nothing matters\",\"don't care anymore\",\"given up\",\"giving up\",\"helpless\",\"worthless\",\"useless\",\"hate myself\",\"no one cares\",\"nobody cares\",\"no one understands\",\"nobody understands\",\"empty inside\",\"can't stop crying\",\"breaking point\",\"at my limit\",\"having a breakdown\""]" } // ISSUE 5 TODO: phrase lists are rebuilt from JSON literals on every call. @@ -284,6 +284,26 @@ fn safety_count_match(text: String, phrases_json: String) -> Int { // Returns "none" | "soft" | "hard". Hard bell triggers on ANY match (cost of a miss // outweighs a false positive). Soft bell needs >= 2 matches to reduce false positives. +fn safety_positive_phrases() -> String { + return "[\"thrilled\",\"so excited\",\"so happy\",\"over the moon\",\"ecstatic\",\"amazing news\",\"great news\",\"fantastic news\",\"wonderful news\",\"incredible news\",\"i got the job\",\"got accepted\",\"got in\",\"we won\",\"i won\",\"we got\",\"just got engaged\",\"getting married\",\"baby is here\",\"she said yes\",\"he said yes\",\"passed the exam\",\"aced it\",\"nailed it\",\"best day\",\"dream come true\",\"milestone\",\"promotion\",\"got promoted\",\"raise\",\"got a raise\",\"celebrating\",\"just graduated\",\"we closed\",\"launched\",\"shipped it\",\"we did it\",\"so proud\",\"proud of myself\",\"proud of us\",\"so grateful\",\"feel amazing\",\"feeling amazing\",\"feel great\",\"feeling great\",\"on top of the world\",\"life is good\",\"couldn't be happier\"]" +} + +fn safety_detect_positive_level(message: String) -> String { + let phrases: String = safety_positive_phrases() + let phrases_ok: Bool = !str_eq(phrases, "") && !str_eq(phrases, "[]") + if !phrases_ok { return "none" } + let n: Int = json_array_len(phrases) + let i: Int = 0 + while i < n { + let phrase: String = json_array_get(phrases, i) + if str_contains(message, phrase) { + return "high" + } + let i = i + 1 + } + return "none" +} + fn safety_detect_bell_level(message: String) -> String { let text: String = safety_normalize(message) let is_hard: Bool = safety_any_match(text, safety_self_harm_phrases()) diff --git a/soul.el b/soul.el index c58b03d..066dd8a 100644 --- a/soul.el +++ b/soul.el @@ -162,6 +162,75 @@ fn load_identity_context() -> Void { println("[soul] persona node loaded (" + int_to_str(str_len(p_content)) + " chars)") } } + + // Cross-session affective context: load BellEvent and PositiveEvent nodes from last 7 days. + let aff_now: Int = time_now() + let aff_7d: Int = aff_now - 604800 + let bell_raw: String = engram_search_json("bell:soft bell:hard BellEvent affective", 3) + let bell_aff_ok: Bool = !str_eq(bell_raw, "") && !str_eq(bell_raw, "[]") + let aff_ctx: String = "" + let aff_ctx = if bell_aff_ok { + let bn_total: Int = json_array_len(bell_raw) + let bacc: String = "" + let bi: Int = 0 + let bacc = while bi < bn_total { + let bn: String = json_array_get(bell_raw, bi) + let bn_c: String = json_get(bn, "content") + let bm: String = " | ts:" + let bmp: Int = str_index_of(bn_c, bm) + let bn_ts_raw: String = if bmp >= 0 { + let bs: Int = bmp + str_len(bm) + let br: String = str_slice(bn_c, bs, str_len(bn_c)) + let bn_next: Int = str_index_of(br, " | ") + if bn_next < 0 { br } else { str_slice(br, 0, bn_next) } + } else { + let bca: String = json_get(bn, "created_at") + if str_eq(bca, "") { json_get(bn, "updated_at") } else { bca } + } + let bn_ts: Int = if str_eq(bn_ts_raw, "") { 0 } else { str_to_int(bn_ts_raw) } + let snip: String = if str_len(bn_c) > 200 { str_slice(bn_c, 0, 200) } else { bn_c } + let bacc = if bn_ts >= aff_7d && !str_eq(snip, "") { + if str_eq(bacc, "") { snip } else { bacc + "\n" + snip } + } else { bacc } + let bi = bi + 1 + bacc + } + bacc + } else { "" } + let pos_raw: String = engram_search_json("PositiveEvent joy:high joy:low affective", 3) + let pos_aff_ok: Bool = !str_eq(pos_raw, "") && !str_eq(pos_raw, "[]") + let aff_ctx = if pos_aff_ok { + let pn_total: Int = json_array_len(pos_raw) + let pacc: String = aff_ctx + let pi: Int = 0 + let pacc = while pi < pn_total { + let pn: String = json_array_get(pos_raw, pi) + let pn_c: String = json_get(pn, "content") + let pm: String = " | ts:" + let pmp: Int = str_index_of(pn_c, pm) + let pn_ts_raw: String = if pmp >= 0 { + let ps: Int = pmp + str_len(pm) + let pr: String = str_slice(pn_c, ps, str_len(pn_c)) + let pn_next: Int = str_index_of(pr, " | ") + if pn_next < 0 { pr } else { str_slice(pr, 0, pn_next) } + } else { + let pca: String = json_get(pn, "created_at") + if str_eq(pca, "") { json_get(pn, "updated_at") } else { pca } + } + let pn_ts: Int = if str_eq(pn_ts_raw, "") { 0 } else { str_to_int(pn_ts_raw) } + let psnip: String = if str_len(pn_c) > 200 { str_slice(pn_c, 0, 200) } else { pn_c } + let pacc = if pn_ts >= aff_7d && !str_eq(psnip, "") { + if str_eq(pacc, "") { psnip } else { pacc + "\n" + psnip } + } else { pacc } + let pi = pi + 1 + pacc + } + pacc + } else { aff_ctx } + if !str_eq(aff_ctx, "") { + state_set("soul_affective_context", aff_ctx) + println("[soul] 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. @@ -320,14 +389,53 @@ fn layered_cycle(raw_input: String) -> String { json_get(steward_result, "redirect_to") } - // ISSUE 1: pre-LLM bell augmentation for layered_cycle path. - // safety_augment_system appends soft/hard directive to system prompt when bell fires, - // ensuring LLM processes message WITH the safety directive -- not just post-output gate. - // Stored in state as "layered_cycle_safety_system_addendum" for imprint_respond to use. - // TODO: wire directly when imprint_respond gains system_override param (imprint.el change). - // ISSUE 3 TODO: no semantic crisis detection. Keyword-only means signals that evade - // the phrase list pass with zero augmentation. Semantic layer = separate decision. + // L2c: affective context injection. + let lc_aff_cutoff: Int = time_now() - 259200 + let lc_bell_nodes: String = engram_search_json("bell:soft bell:hard BellEvent affective", 2) + let lc_has_bell: Bool = !str_eq(lc_bell_nodes, "") && !str_eq(lc_bell_nodes, "[]") + let lc_bell_note: String = if lc_has_bell { + let lb0: String = json_array_get(lc_bell_nodes, 0) + let lb_c: String = json_get(lb0, "content") + let lbm: String = " | ts:" + let lbmp: Int = str_index_of(lb_c, lbm) + let lb_ts_raw: String = if lbmp >= 0 { + let lbs: Int = lbmp + str_len(lbm) + let lbr: String = str_slice(lb_c, lbs, str_len(lb_c)) + let lbn: Int = str_index_of(lbr, " | ") + if lbn < 0 { lbr } else { str_slice(lbr, 0, lbn) } + } else { + let lbca: String = json_get(lb0, "created_at") + if str_eq(lbca, "") { json_get(lb0, "updated_at") } else { lbca } + } + let lb_ts: Int = if str_eq(lb_ts_raw, "") { 0 } else { str_to_int(lb_ts_raw) } + if lb_ts > lc_aff_cutoff { "[AFFECTIVE NOTE: User was in distress in a recent session.]" } else { "" } + } else { "" } + let lc_pos_nodes: String = engram_search_json("PositiveEvent joy:high joy:low affective", 2) + let lc_has_pos: Bool = !str_eq(lc_pos_nodes, "") && !str_eq(lc_pos_nodes, "[]") + let lc_pos_note: String = if lc_has_pos && str_eq(lc_bell_note, "") { + let lp0: String = json_array_get(lc_pos_nodes, 0) + let lp_c: String = json_get(lp0, "content") + let lpm: String = " | ts:" + let lpmp: Int = str_index_of(lp_c, lpm) + let lp_ts_raw: String = if lpmp >= 0 { + let lps: Int = lpmp + str_len(lpm) + let lpr: String = str_slice(lp_c, lps, str_len(lp_c)) + let lpn: Int = str_index_of(lpr, " | ") + if lpn < 0 { lpr } else { str_slice(lpr, 0, lpn) } + } else { + let lpca: String = json_get(lp0, "created_at") + if str_eq(lpca, "") { json_get(lp0, "updated_at") } else { lpca } + } + let lp_ts: Int = if str_eq(lp_ts_raw, "") { 0 } else { str_to_int(lp_ts_raw) } + if lp_ts > lc_aff_cutoff { "[AFFECTIVE NOTE: User shared positive news in a recent session.]" } else { "" } + } else { "" } + let lc_affective_note: String = if !str_eq(lc_bell_note, "") { lc_bell_note } else { lc_pos_note } + + // pre-LLM bell augmentation let augmented_addendum: String = safety_augment_system("", raw_input) + let augmented_addendum = if str_eq(lc_affective_note, "") { augmented_addendum } else { + if str_eq(augmented_addendum, "") { lc_affective_note } else { lc_affective_note + "\n" + augmented_addendum } + } state_set("layered_cycle_safety_system_addendum", augmented_addendum) // L3: imprint responds