Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 96d6bef0c2 | |||
| 76c2e47d0f |
@@ -12,47 +12,113 @@ fn chat_default_model() -> String {
|
|||||||
return "claude-sonnet-4-5"
|
return "claude-sonnet-4-5"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parse_salience_100 — convert a %g-serialized float to integer * 100.
|
||||||
|
// The C runtime serializes floats with %g which trims trailing zeros:
|
||||||
|
// 0.70 → "0.7", 0.60 → "0.6", 0.50 → "0.5", 1.0 → "1"
|
||||||
|
// The naive str_replace(".", "") approach breaks for single-decimal strings:
|
||||||
|
// "0.7" → "07" → str_to_int → 7 (WRONG, should be 70)
|
||||||
|
// "0.5" → "05" → str_to_int → 5 (WRONG, should be 50)
|
||||||
|
// "0.85" → "085" → str_to_int → 85 (accidentally correct — two decimal digits)
|
||||||
|
// Fix: use str_index_of to find the decimal point and scale accordingly:
|
||||||
|
// No decimal ("1"): multiply raw by 100
|
||||||
|
// One decimal digit ("0.7"): multiply stripped value by 10
|
||||||
|
// Two+ decimal digits ("0.85"): stripped value is already in hundredths
|
||||||
|
fn parse_salience_100(s: String) -> Int {
|
||||||
|
if str_eq(s, "") { return 70 }
|
||||||
|
let dot_pos: Int = str_index_of(s, ".")
|
||||||
|
let raw: Int = if dot_pos < 0 {
|
||||||
|
// No decimal point — integer like "1" means 100%
|
||||||
|
str_to_int(s) * 100
|
||||||
|
} else {
|
||||||
|
let after_dot: String = str_slice(s, dot_pos + 1, str_len(s))
|
||||||
|
let decimal_digits: Int = str_len(after_dot)
|
||||||
|
let stripped: Int = str_to_int(str_replace(s, ".", ""))
|
||||||
|
if decimal_digits == 1 { stripped * 10 } else { stripped }
|
||||||
|
}
|
||||||
|
if raw > 100 { 100 } else { if raw < 0 { 0 } else { raw } }
|
||||||
|
}
|
||||||
|
|
||||||
// engram_score_node — compute a recency x relevance score for a single engram
|
// engram_score_node — compute a recency x relevance score for a single engram
|
||||||
// node JSON object. Higher is better. Score = salience * importance * recency_factor.
|
// node JSON object. Higher is better.
|
||||||
// recency_factor decays linearly over 30 days: nodes updated today score 1.0,
|
//
|
||||||
// nodes 30+ days old score 0.1 (floor). Nodes with no created_at score 0.5.
|
// Bugs fixed vs original implementation:
|
||||||
// This keeps fresh, high-salience nodes at the top and pushes stale low-signal
|
// 1. FLOAT PARSING: parse_salience_100 correctly handles %g single-decimal output.
|
||||||
// nodes to the bottom so they get trimmed when we cap context size.
|
// "0.7" → 70, "0.6" → 60, "0.5" → 50 (was: 7, 6, 5 — scored near zero and
|
||||||
|
// were filtered by threshold=25, making the function broken for the majority
|
||||||
|
// of the graph where conv/utterance nodes have salience/importance ≈ 0.6/0.7).
|
||||||
|
// 2. RECENCY USES LAST TOUCH: uses max(created_at, updated_at, last_activated) so
|
||||||
|
// nodes strengthened by engram_strengthen() after chat turns are not penalised
|
||||||
|
// for a stale created_at. A node referenced yesterday but created 25 days ago
|
||||||
|
// now correctly scores as fresh rather than borderline-filtered.
|
||||||
|
// 3. COMPRESSED RECENCY RANGE: old formula (sal * imp * recency / 10000) gave
|
||||||
|
// recency a 10x dynamic range (10-100) vs 1.9x for salience/importance. A
|
||||||
|
// canonical high-importance node at 30 days scored the same as a fresh noise
|
||||||
|
// node. New formula compresses recency to 1.54x via (50 + recency/2) weight.
|
||||||
|
// 4. SOFTER FLOOR: recency floor raised from 10 to 30 with tier-aware decay windows
|
||||||
|
// so canonical identity/persona nodes never bottom out to near-zero.
|
||||||
fn engram_score_node(node_json: String) -> Int {
|
fn engram_score_node(node_json: String) -> Int {
|
||||||
let salience_str: String = json_get(node_json, "salience")
|
let salience_str: String = json_get(node_json, "salience")
|
||||||
let importance_str: String = json_get(node_json, "importance")
|
let importance_str: String = json_get(node_json, "importance")
|
||||||
let created_str: String = json_get(node_json, "created_at")
|
let created_str: String = json_get(node_json, "created_at")
|
||||||
|
let updated_str: String = json_get(node_json, "updated_at")
|
||||||
|
let activated_str: String = json_get(node_json, "last_activated")
|
||||||
|
let tier_str: String = json_get(node_json, "tier")
|
||||||
|
|
||||||
// Parse as floats via * 100 integer arithmetic (el has no float math)
|
// parse_salience_100 handles "0.7" → 70, "0.85" → 85, "1.0" → 100, "1" → 100
|
||||||
let salience_100: Int = if str_eq(salience_str, "") { 70 } else {
|
let salience_100: Int = parse_salience_100(salience_str)
|
||||||
let s: Int = str_to_int(str_replace(salience_str, ".", ""))
|
let importance_100: Int = parse_salience_100(importance_str)
|
||||||
// Clamp to 0-100 range (value was e.g. "0.85" -> parsed "085" = 85)
|
|
||||||
if s > 100 { 100 } else { if s < 0 { 0 } else { s } }
|
|
||||||
}
|
|
||||||
let importance_100: Int = if str_eq(importance_str, "") { 70 } else {
|
|
||||||
let v: Int = str_to_int(str_replace(importance_str, ".", ""))
|
|
||||||
if v > 100 { 100 } else { if v < 0 { 0 } else { v } }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recency: decay from 100 (today) to 10 (30+ days). created_at is Unix seconds.
|
// Recency: use max(created_at, updated_at, last_activated).
|
||||||
|
// last_activated is updated by engram_strengthen() every chat turn — nodes
|
||||||
|
// actively referenced score fresh regardless of original write time.
|
||||||
let now_ts: Int = time_now()
|
let now_ts: Int = time_now()
|
||||||
let recency_100: Int = if str_eq(created_str, "") { 50 } else {
|
let created_ts: Int = if str_eq(created_str, "") { 0 } else { str_to_int(created_str) }
|
||||||
let created_ts: Int = str_to_int(created_str)
|
let updated_ts: Int = if str_eq(updated_str, "") { 0 } else { str_to_int(updated_str) }
|
||||||
let age_secs: Int = now_ts - created_ts
|
let activated_ts: Int = if str_eq(activated_str, "") { 0 } else { str_to_int(activated_str) }
|
||||||
let age_days: Int = age_secs / 86400
|
let best_ts_ab: Int = if updated_ts > created_ts { updated_ts } else { created_ts }
|
||||||
let decay: Int = if age_days >= 30 { 10 } else { 100 - (age_days * 3) }
|
let best_ts: Int = if activated_ts > best_ts_ab { activated_ts } else { best_ts_ab }
|
||||||
if decay < 10 { 10 } else { decay }
|
let recency_100: Int = if best_ts == 0 { 50 } else {
|
||||||
|
let age_secs: Int = now_ts - best_ts
|
||||||
|
// Guard against clock skew (future timestamps): treat as brand new.
|
||||||
|
let age_days: Int = if age_secs < 0 { 0 } else { age_secs / 86400 }
|
||||||
|
// Tier-aware decay, softer floor (30 not 10):
|
||||||
|
// Canonical: 365-day window — foundational identity/persona nodes.
|
||||||
|
// Episodic: 90-day window — conversation context fades moderately.
|
||||||
|
// Working/untiered: 35-day window — transient task state.
|
||||||
|
let is_canonical: Bool = str_eq(tier_str, "Canonical")
|
||||||
|
let is_episodic: Bool = str_eq(tier_str, "Episodic")
|
||||||
|
let decay: Int = if is_canonical {
|
||||||
|
let drop: Int = if age_days >= 365 { 70 } else { age_days * 70 / 365 }
|
||||||
|
100 - drop
|
||||||
|
} else {
|
||||||
|
if is_episodic {
|
||||||
|
if age_days >= 90 { 30 } else { 100 - (age_days * 70 / 90) }
|
||||||
|
} else {
|
||||||
|
if age_days >= 35 { 30 } else { 100 - (age_days * 2) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if decay < 30 { 30 } else { decay }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combined score 0-1000000 (no floats): salience * importance * recency / 10000
|
// Compressed recency weight (50 + recency/2): range 65-100 (1.54x dynamic range).
|
||||||
return salience_100 * importance_100 * recency_100 / 10000
|
// Old formula had 10x recency range which drowned out relevance for old-but-important
|
||||||
|
// nodes. New: relevance (0-100) × recency_weight (65-100) / 100 → score 0-100.
|
||||||
|
// salience_100 and importance_100 are already in the 0-100 range (parse_salience_100
|
||||||
|
// returns e.g. 70 for "0.7"). Dividing by 100 keeps relevance in 0-100.
|
||||||
|
// Dividing by 10000 caused integer truncation to 0 for all real-world nodes
|
||||||
|
// (e.g., sal=0.7, imp=0.7 → 70*70/10000 = 0 instead of 49).
|
||||||
|
let relevance: Int = salience_100 * importance_100 / 100
|
||||||
|
let recency_weight: Int = 50 + recency_100 / 2
|
||||||
|
return relevance * recency_weight / 100
|
||||||
}
|
}
|
||||||
|
|
||||||
// engram_compile_ranked — build a context string from a JSON array of node objects,
|
// engram_compile_ranked — build a context string from a JSON array of node objects,
|
||||||
// ordered best-first by score. Only nodes above a minimum score (25 = salience 0.5 *
|
// ordered best-first by score. Only nodes above threshold=10 are included.
|
||||||
// importance 0.5 * recency 1.0) are included; the rest are noise. Returns at most
|
// With corrected formula (sal*imp/100): sal=0.5*imp=0.5 at max recency scores 25;
|
||||||
// max_nodes entries concatenated as JSON array text. Because el has no sort primitive,
|
// sal=0.5*imp=0.5 at Working floor (recency=30, weight=65) scores 16.
|
||||||
// we do a single selection pass picking the top N by linear scan (N=10 cap).
|
// Threshold=10 gives safe headroom for low-salience nodes near the recency floor,
|
||||||
|
// while still filtering near-zero noise (e.g., sal=0.1*imp=0.1 → score≤1).
|
||||||
|
// Returns at most max_nodes entries. max_nodes must not exceed 20 (sentinel limit).
|
||||||
fn engram_compile_ranked(nodes_json: String, max_nodes: Int) -> String {
|
fn engram_compile_ranked(nodes_json: String, max_nodes: Int) -> String {
|
||||||
if str_eq(nodes_json, "") { return "" }
|
if str_eq(nodes_json, "") { return "" }
|
||||||
if str_eq(nodes_json, "[]") { return "" }
|
if str_eq(nodes_json, "[]") { return "" }
|
||||||
@@ -73,8 +139,10 @@ fn engram_compile_ranked(nodes_json: String, max_nodes: Int) -> String {
|
|||||||
while ci < total {
|
while ci < total {
|
||||||
let node: String = json_array_get(nodes_json, ci)
|
let node: String = json_array_get(nodes_json, ci)
|
||||||
let score: Int = engram_score_node(node)
|
let score: Int = engram_score_node(node)
|
||||||
// Only include reasonably relevant nodes (threshold=25)
|
// Threshold=10: allows moderately-relevant older nodes while filtering noise.
|
||||||
let above_thresh: Bool = score >= 25
|
// Example: sal=0.5 imp=0.5 at Working recency floor (35+ days) → score 16,
|
||||||
|
// which passes. A near-zero node (sal=0.1 imp=0.1) → score ≤ 1, filtered.
|
||||||
|
let above_thresh: Bool = score >= 10
|
||||||
// Check this index wasn't already selected (sentinel: look for idx marker)
|
// Check this index wasn't already selected (sentinel: look for idx marker)
|
||||||
let idx_marker: String = "\"_sel_" + int_to_str(ci) + "\""
|
let idx_marker: String = "\"_sel_" + int_to_str(ci) + "\""
|
||||||
let already_picked: Bool = str_contains(selected, idx_marker)
|
let already_picked: Bool = str_contains(selected, idx_marker)
|
||||||
@@ -101,7 +169,7 @@ fn engram_compile_ranked(nodes_json: String, max_nodes: Int) -> String {
|
|||||||
// Strip the _sel_N sentinel fields that were used for duplicate-detection bookkeeping.
|
// Strip the _sel_N sentinel fields that were used for duplicate-detection bookkeeping.
|
||||||
// The sentinels have the form "\"_sel_N\":1," (trailing comma, space before next key).
|
// The sentinels have the form "\"_sel_N\":1," (trailing comma, space before next key).
|
||||||
// We injected them as the first field in each object, so the pattern is predictable.
|
// We injected them as the first field in each object, so the pattern is predictable.
|
||||||
// Because el has no regex, remove up to 10 possible sentinel variants by literal replace.
|
// Because el has no regex, remove up to 20 possible sentinel variants by literal replace.
|
||||||
let clean: String = "[" + selected + "]"
|
let clean: String = "[" + selected + "]"
|
||||||
let c0: String = str_replace(clean, "\"_sel_0\":1,", "")
|
let c0: String = str_replace(clean, "\"_sel_0\":1,", "")
|
||||||
let c1: String = str_replace(c0, "\"_sel_1\":1,", "")
|
let c1: String = str_replace(c0, "\"_sel_1\":1,", "")
|
||||||
@@ -113,7 +181,17 @@ fn engram_compile_ranked(nodes_json: String, max_nodes: Int) -> String {
|
|||||||
let c7: String = str_replace(c6, "\"_sel_7\":1,", "")
|
let c7: String = str_replace(c6, "\"_sel_7\":1,", "")
|
||||||
let c8: String = str_replace(c7, "\"_sel_8\":1,", "")
|
let c8: String = str_replace(c7, "\"_sel_8\":1,", "")
|
||||||
let c9: String = str_replace(c8, "\"_sel_9\":1,", "")
|
let c9: String = str_replace(c8, "\"_sel_9\":1,", "")
|
||||||
return c9
|
let c10: String = str_replace(c9, "\"_sel_10\":1,", "")
|
||||||
|
let c11: String = str_replace(c10, "\"_sel_11\":1,", "")
|
||||||
|
let c12: String = str_replace(c11, "\"_sel_12\":1,", "")
|
||||||
|
let c13: String = str_replace(c12, "\"_sel_13\":1,", "")
|
||||||
|
let c14: String = str_replace(c13, "\"_sel_14\":1,", "")
|
||||||
|
let c15: String = str_replace(c14, "\"_sel_15\":1,", "")
|
||||||
|
let c16: String = str_replace(c15, "\"_sel_16\":1,", "")
|
||||||
|
let c17: String = str_replace(c16, "\"_sel_17\":1,", "")
|
||||||
|
let c18: String = str_replace(c17, "\"_sel_18\":1,", "")
|
||||||
|
let c19: String = str_replace(c18, "\"_sel_19\":1,", "")
|
||||||
|
return c19
|
||||||
}
|
}
|
||||||
|
|
||||||
fn engram_compile(intent: String) -> String {
|
fn engram_compile(intent: String) -> String {
|
||||||
@@ -124,8 +202,11 @@ fn engram_compile(intent: String) -> String {
|
|||||||
let act_ok: Bool = !str_eq(activate_json, "") && !str_eq(activate_json, "[]")
|
let act_ok: Bool = !str_eq(activate_json, "") && !str_eq(activate_json, "[]")
|
||||||
let srch_ok: Bool = !str_eq(search_json, "") && !str_eq(search_json, "[]")
|
let srch_ok: Bool = !str_eq(search_json, "") && !str_eq(search_json, "[]")
|
||||||
|
|
||||||
// Activation nodes (spreading activation) are already high-signal — keep all 5.
|
// Activation nodes (spreading activation) are high-signal but apply scoring via
|
||||||
let act_part: String = if act_ok { activate_json } else { "" }
|
// engram_compile_ranked with threshold=5 to exclude genuinely zero-quality stale
|
||||||
|
// nodes that happen to be graph-connected. The threshold of 5 is well below the
|
||||||
|
// search path threshold of 15 to preserve the activation path's higher recall.
|
||||||
|
let act_part: String = if act_ok { engram_compile_ranked(activate_json, 5) } else { "" }
|
||||||
|
|
||||||
// Rank search results and keep only the top 8 (was: flat 15 unranked).
|
// Rank search results and keep only the top 8 (was: flat 15 unranked).
|
||||||
// This cuts context noise roughly in half while preserving the best-scoring nodes.
|
// This cuts context noise roughly in half while preserving the best-scoring nodes.
|
||||||
@@ -181,31 +262,7 @@ fn engram_compile(intent: String) -> String {
|
|||||||
let bn_ts: Int = if str_eq(bn_ts_raw, "") { 0 } else { str_to_int(bn_ts_raw) }
|
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 { "" }
|
if bn_ts > cutoff_ts { bn0 } else { "" }
|
||||||
} else { "" }
|
} else { "" }
|
||||||
// Positive emotion context: check for recent joy/success moments within 72h.
|
let affective_part: String = if !str_eq(recent_bell, "") { recent_bell } else { "" }
|
||||||
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 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 { "" }
|
let sep2: String = if (!str_eq(act_part, "") || !str_eq(srch_part, "")) && !str_eq(scan_part, "") { "\n" } else { "" }
|
||||||
@@ -265,7 +322,7 @@ fn build_system_prompt(ctx: String) -> String {
|
|||||||
safety_addendum
|
safety_addendum
|
||||||
}
|
}
|
||||||
|
|
||||||
return identity + date_line + voice_rules + security_rules + capability_rules + identity_block + affective_boot_block + engram_block + safety_block
|
return identity + date_line + voice_rules + security_rules + capability_rules + identity_block + engram_block + safety_block
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hist_append(hist: String, role: String, content: String) -> String {
|
fn hist_append(hist: String, role: String, content: String) -> String {
|
||||||
@@ -424,62 +481,22 @@ fn handle_chat(body: String) -> String {
|
|||||||
|
|
||||||
// Cross-session affective context: on session start (no history yet), check engram
|
// 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.
|
// for recent distress signals within 72h and prepend a care directive if found.
|
||||||
let affective_prefix: String = {
|
let affective_prefix: String = if hist_len == 0 {
|
||||||
// Runs every turn. Uses correct BellEvent/PositiveEvent tags.
|
let distress_nodes: String = engram_search_json("bell distress crisis loss grief despair", 3)
|
||||||
let aff_now_ts: Int = time_now()
|
let has_nodes: Bool = !str_eq(distress_nodes, "") && !str_eq(distress_nodes, "[]")
|
||||||
let aff_cutoff: Int = aff_now_ts - 259200
|
let now_ts: Int = time_now()
|
||||||
let boot_aff: String = state_get("soul_affective_context")
|
let cutoff: Int = now_ts - 259200
|
||||||
let has_boot_aff: Bool = !str_eq(boot_aff, "")
|
let found_recent: Bool = if has_nodes {
|
||||||
let dist_nodes_aff: String = engram_search_json("bell:soft bell:hard BellEvent affective", 3)
|
let dn0: String = json_array_get(distress_nodes, 0)
|
||||||
let has_dist_aff: Bool = !str_eq(dist_nodes_aff, "") && !str_eq(dist_nodes_aff, "[]")
|
let ts0_raw: String = json_get(dn0, "created_at")
|
||||||
let found_recent_dist: Bool = if has_boot_aff {
|
let ts0_str: String = if str_eq(ts0_raw, "") { json_get(dn0, "updated_at") } else { ts0_raw }
|
||||||
true
|
let ts0: Int = if str_eq(ts0_str, "") { 0 } else { str_to_int(ts0_str) }
|
||||||
} else {
|
ts0 > cutoff
|
||||||
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 }
|
} else { false }
|
||||||
if found_recent_dist {
|
if found_recent {
|
||||||
"[RECENT CONTEXT: User recently expressed significant distress. Monitor for indirect crisis signals and respond with care.]\n\n"
|
"[RECENT CONTEXT: User recently expressed significant distress. Monitor for indirect crisis signals and respond with care.]\n\n"
|
||||||
} else {
|
} else { "" }
|
||||||
if found_recent_pos {
|
} else { "" }
|
||||||
"[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 ctx: String = engram_compile(activation_seed)
|
||||||
let system: String = affective_prefix + build_system_prompt(ctx)
|
let system: String = affective_prefix + build_system_prompt(ctx)
|
||||||
@@ -1038,7 +1055,7 @@ fn handle_chat_agentic(body: String) -> String {
|
|||||||
if str_eq(screen_action, "hard_bell") {
|
if str_eq(screen_action, "hard_bell") {
|
||||||
safety_log_bell("hard", json_get(screen_result, "reason"), str_slice(message, 0, 80))
|
safety_log_bell("hard", json_get(screen_result, "reason"), str_slice(message, 0, 80))
|
||||||
return "{\"reply\":\"" + json_safe(safety_validate("", "hard_bell")) + "\",\"model\":\"\",\"agentic\":true,\"tools_used\":[]}"
|
return "{\"reply\":\"" + json_safe(safety_validate("", "hard_bell")) + "\",\"model\":\"\",\"agentic\":true,\"tools_used\":[]}"
|
||||||
|
}
|
||||||
|
|
||||||
let req_model: String = json_get(body, "model")
|
let req_model: String = json_get(body, "model")
|
||||||
let model: String = if str_eq(req_model, "") { chat_default_model() } else { req_model }
|
let model: String = if str_eq(req_model, "") { chat_default_model() } else { req_model }
|
||||||
@@ -1574,18 +1591,13 @@ fn auto_persist(req: String, resp: String) -> Void {
|
|||||||
// consistent with what safety_screen already evaluated for this turn.
|
// consistent with what safety_screen already evaluated for this turn.
|
||||||
let bell_level: String = safety_detect_bell_level(message)
|
let bell_level: String = safety_detect_bell_level(message)
|
||||||
let is_bell: Bool = !str_eq(bell_level, "none")
|
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 affective metadata when emotion is detected.
|
// Tag the Conversation node with bell metadata when distress is present so
|
||||||
|
// subsequent affective queries (e.g. engram_compile) can find this exchange.
|
||||||
let tags: String = if is_bell {
|
let tags: String = if is_bell {
|
||||||
"[\"Conversation\",\"chat\",\"timestamped\",\"bell:" + bell_level + "\",\"affective\"]"
|
"[\"Conversation\",\"chat\",\"timestamped\",\"bell:" + bell_level + "\",\"affective\"]"
|
||||||
} else {
|
} else {
|
||||||
if is_positive {
|
"[\"Conversation\",\"chat\",\"timestamped\"]"
|
||||||
"[\"Conversation\",\"chat\",\"timestamped\",\"joy:" + positive_level + "\",\"affective\"]"
|
|
||||||
} else {
|
|
||||||
"[\"Conversation\",\"chat\",\"timestamped\"]"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let content: String = "{\"q\":\"" + safe_msg + "\""
|
let content: String = "{\"q\":\"" + safe_msg + "\""
|
||||||
@@ -1671,28 +1683,6 @@ fn auto_persist(req: String, resp: String) -> Void {
|
|||||||
}
|
}
|
||||||
state_set(signal_key, safe_summary)
|
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.
|
// strengthen_chat_nodes — strengthen the engram nodes that were activated during a chat.
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ fn safety_general_hard_phrases() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn safety_soft_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\""]"
|
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\"]"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ISSUE 5 TODO: phrase lists are rebuilt from JSON literals on every call.
|
// ISSUE 5 TODO: phrase lists are rebuilt from JSON literals on every call.
|
||||||
@@ -284,26 +284,6 @@ fn safety_count_match(text: String, phrases_json: String) -> Int {
|
|||||||
|
|
||||||
// Returns "none" | "soft" | "hard". Hard bell triggers on ANY match (cost of a miss
|
// 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.
|
// 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 {
|
fn safety_detect_bell_level(message: String) -> String {
|
||||||
let text: String = safety_normalize(message)
|
let text: String = safety_normalize(message)
|
||||||
let is_hard: Bool = safety_any_match(text, safety_self_harm_phrases())
|
let is_hard: Bool = safety_any_match(text, safety_self_harm_phrases())
|
||||||
|
|||||||
@@ -162,75 +162,6 @@ fn load_identity_context() -> Void {
|
|||||||
println("[soul] persona node loaded (" + int_to_str(str_len(p_content)) + " chars)")
|
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.
|
// seed_persona_from_env — one-time migration: SOUL_IDENTITY env var → Persona graph node.
|
||||||
@@ -389,53 +320,14 @@ fn layered_cycle(raw_input: String) -> String {
|
|||||||
json_get(steward_result, "redirect_to")
|
json_get(steward_result, "redirect_to")
|
||||||
}
|
}
|
||||||
|
|
||||||
// L2c: affective context injection.
|
// ISSUE 1: pre-LLM bell augmentation for layered_cycle path.
|
||||||
let lc_aff_cutoff: Int = time_now() - 259200
|
// safety_augment_system appends soft/hard directive to system prompt when bell fires,
|
||||||
let lc_bell_nodes: String = engram_search_json("bell:soft bell:hard BellEvent affective", 2)
|
// ensuring LLM processes message WITH the safety directive -- not just post-output gate.
|
||||||
let lc_has_bell: Bool = !str_eq(lc_bell_nodes, "") && !str_eq(lc_bell_nodes, "[]")
|
// Stored in state as "layered_cycle_safety_system_addendum" for imprint_respond to use.
|
||||||
let lc_bell_note: String = if lc_has_bell {
|
// TODO: wire directly when imprint_respond gains system_override param (imprint.el change).
|
||||||
let lb0: String = json_array_get(lc_bell_nodes, 0)
|
// ISSUE 3 TODO: no semantic crisis detection. Keyword-only means signals that evade
|
||||||
let lb_c: String = json_get(lb0, "content")
|
// the phrase list pass with zero augmentation. Semantic layer = separate decision.
|
||||||
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: 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)
|
state_set("layered_cycle_safety_system_addendum", augmented_addendum)
|
||||||
|
|
||||||
// L3: imprint responds
|
// L3: imprint responds
|
||||||
|
|||||||
Reference in New Issue
Block a user