Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 42bbadcd33 | |||
| b6052f9de3 | |||
| 0113407728 | |||
| 18e040acb1 | |||
| 21f248a33a | |||
| aef687b57c | |||
| 6edf9937dd | |||
| e447a87a00 | |||
| 575ff1329a | |||
| db33b0cb91 | |||
| f35569d4bb | |||
| 94b71b6e6b | |||
| 392d2416ec | |||
| e6da638536 | |||
| 2865d6ad26 | |||
| 47d0e6f985 | |||
| d008649c3e | |||
| aa70c5dde6 | |||
| deddb9a18e | |||
| 494d973a3b | |||
| 34551695a1 | |||
| 615f0cee08 |
@@ -678,6 +678,8 @@ fn threat_trajectory_check(tool_name: String, tool_input: String) -> Int {
|
|||||||
return combined
|
return combined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(reliability #10): agentic_conv_history is process-global; awareness loop
|
||||||
|
// and HTTP workers race on this key. Impact: noisy threat score only, not content.
|
||||||
fn threat_history_append(text: String) -> Void {
|
fn threat_history_append(text: String) -> Void {
|
||||||
let current: String = state_get("agentic_conv_history")
|
let current: String = state_get("agentic_conv_history")
|
||||||
let safe_text: String = str_to_lower(text)
|
let safe_text: String = str_to_lower(text)
|
||||||
|
|||||||
@@ -265,174 +265,6 @@ fn engram_nodes_merge(a: String, b: String) -> String {
|
|||||||
return engram_dedup_nodes("[" + ai + "," + bi + "]")
|
return engram_dedup_nodes("[" + ai + "," + bi + "]")
|
||||||
}
|
}
|
||||||
|
|
||||||
// is_followup_phrase — recognize explicit follow-up references that are too short
|
|
||||||
// to carry meaningful topic signal on their own. These should activate on the
|
|
||||||
// prior thread context rather than the bare message. Fixes Issues 2/8.
|
|
||||||
fn is_followup_phrase(msg: String) -> Bool {
|
|
||||||
if str_contains(msg, "tell me more") { return true }
|
|
||||||
if str_contains(msg, "elaborate") { return true }
|
|
||||||
if str_contains(msg, "go on") { return true }
|
|
||||||
if str_contains(msg, "what about that") { return true }
|
|
||||||
if str_contains(msg, "what else") { return true }
|
|
||||||
if str_contains(msg, "keep going") { return true }
|
|
||||||
if str_contains(msg, "more detail") { return true }
|
|
||||||
if str_contains(msg, "last part") { return true }
|
|
||||||
if str_contains(msg, "say more") { return true }
|
|
||||||
if str_eq(msg, "ok") { return true }
|
|
||||||
if str_eq(msg, "yes") { return true }
|
|
||||||
if str_eq(msg, "yeah") { return true }
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// engram_is_continuation — semantic continuation detection for recall activation.
|
|
||||||
// Fixes Issue 2: the old 50-char threshold was brittle — short messages that
|
|
||||||
// introduce new topics (e.g. "sleep" or "AWS") were treated as continuations.
|
|
||||||
// Strategy: combine length heuristic with follow-up phrase detection and
|
|
||||||
// mid-sentence capitalization check (new sentence, probably new topic).
|
|
||||||
fn engram_is_continuation(msg: String, hist_len: Int) -> Bool {
|
|
||||||
if hist_len == 0 { return false }
|
|
||||||
let mlen: Int = str_len(msg)
|
|
||||||
if mlen > 80 { return false }
|
|
||||||
if is_followup_phrase(msg) { return true }
|
|
||||||
// Single-word or very short messages without capitalized new topic
|
|
||||||
if mlen < 20 { return true }
|
|
||||||
// Treat as new topic if message starts with a capital (new sentence) and is > 30 chars
|
|
||||||
let first_char: String = str_slice(msg, 0, 1)
|
|
||||||
let starts_capital: Bool = str_eq(first_char, str_replace(first_char, "abcdefghijklmnopqrstuvwxyz", ""))
|
|
||||||
if starts_capital && mlen > 30 { return false }
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// topic_snip_from_entry — extract the most salient snippet from a history entry.
|
|
||||||
// Fixes Issue 9: the old code sliced from position 0, capturing preamble instead
|
|
||||||
// of the concepts discussed near the end. This takes the TAIL of a long reply
|
|
||||||
// and trims to the last sentence boundary for cleaner semantic anchoring.
|
|
||||||
fn topic_snip_from_entry(content: String) -> String {
|
|
||||||
let clen: Int = str_len(content)
|
|
||||||
if clen <= 200 { return content }
|
|
||||||
let tail: String = str_slice(content, clen - 200, clen)
|
|
||||||
let last_boundary: Int = -1
|
|
||||||
let si: Int = 0
|
|
||||||
let tail_len: Int = str_len(tail)
|
|
||||||
while si < tail_len - 1 {
|
|
||||||
let ch2: String = str_slice(tail, si, si + 2)
|
|
||||||
let is_boundary: Bool = str_eq(ch2, ". ") || str_eq(ch2, ".\n")
|
|
||||||
let last_boundary = if is_boundary { si } else { last_boundary }
|
|
||||||
let si = si + 1
|
|
||||||
}
|
|
||||||
let clean_tail: String = if last_boundary >= 0 {
|
|
||||||
str_slice(tail, last_boundary + 2, tail_len)
|
|
||||||
} else { tail }
|
|
||||||
if str_len(clean_tail) > 150 { return str_slice(clean_tail, 0, 150) }
|
|
||||||
return clean_tail
|
|
||||||
}
|
|
||||||
|
|
||||||
// multi_turn_topic — build a combined topic string from recent user turns.
|
|
||||||
// Fixes Issue 10: a single prior turn in the seed loses earlier high-salience
|
|
||||||
// nodes from multi-turn discussions. This pulls up to 3 prior user turns so
|
|
||||||
// thread continuity survives longer conversations.
|
|
||||||
fn multi_turn_topic(hist: String, hist_len: Int) -> String {
|
|
||||||
if hist_len == 0 { return "" }
|
|
||||||
let topic: String = ""
|
|
||||||
let collected: Int = 0
|
|
||||||
let idx: Int = hist_len - 1
|
|
||||||
while idx >= 0 && collected < 3 {
|
|
||||||
let entry: String = json_array_get(hist, idx)
|
|
||||||
let role: String = json_get(entry, "role")
|
|
||||||
let content: String = json_get(entry, "content")
|
|
||||||
let is_user: Bool = str_eq(role, "user")
|
|
||||||
let snip: String = if str_len(content) > 100 { str_slice(content, 0, 100) } else { content }
|
|
||||||
let topic = if is_user && !str_eq(snip, "") {
|
|
||||||
if str_eq(topic, "") { snip } else { snip + " " + topic }
|
|
||||||
} else { topic }
|
|
||||||
let collected = if is_user { collected + 1 } else { collected }
|
|
||||||
let idx = idx - 1
|
|
||||||
}
|
|
||||||
if str_len(topic) > 300 { return str_slice(topic, 0, 300) }
|
|
||||||
return topic
|
|
||||||
}
|
|
||||||
|
|
||||||
// distill_transcript — extract salient content from a long dharma-room transcript.
|
|
||||||
// Fixes Issue 6: passing the entire transcript produces a diffuse embedding query
|
|
||||||
// where topic signal drowns in context noise. Strategy: last 150 chars (recency)
|
|
||||||
// combined with any question found in the last 500 chars (intent anchoring).
|
|
||||||
fn distill_transcript(transcript: String) -> String {
|
|
||||||
if str_len(transcript) <= 250 { return transcript }
|
|
||||||
let tlen: Int = str_len(transcript)
|
|
||||||
let tail_start: Int = if tlen > 500 { tlen - 500 } else { 0 }
|
|
||||||
let tail: String = str_slice(transcript, tail_start, tlen)
|
|
||||||
let tail_len: Int = str_len(tail)
|
|
||||||
let q_pos: Int = -1
|
|
||||||
let qi: Int = 0
|
|
||||||
while qi < tail_len {
|
|
||||||
let qch: String = str_slice(tail, qi, qi + 1)
|
|
||||||
let q_pos = if str_eq(qch, "?") { qi } else { q_pos }
|
|
||||||
let qi = qi + 1
|
|
||||||
}
|
|
||||||
let q_context: String = if q_pos > 0 {
|
|
||||||
let q_start: Int = if q_pos > 100 { q_pos - 100 } else { 0 }
|
|
||||||
str_slice(tail, q_start, q_pos + 1)
|
|
||||||
} else { "" }
|
|
||||||
let recency_seed: String = if tail_len > 150 {
|
|
||||||
str_slice(tail, tail_len - 150, tail_len)
|
|
||||||
} else { tail }
|
|
||||||
let combined: String = if str_eq(q_context, "") {
|
|
||||||
recency_seed
|
|
||||||
} else {
|
|
||||||
if str_contains(recency_seed, q_context) { recency_seed }
|
|
||||||
else { q_context + " " + recency_seed }
|
|
||||||
}
|
|
||||||
if str_len(combined) > 250 {
|
|
||||||
return str_slice(combined, str_len(combined) - 250, str_len(combined))
|
|
||||||
}
|
|
||||||
return combined
|
|
||||||
}
|
|
||||||
|
|
||||||
// build_activation_seed — construct an enriched activation seed from the current
|
|
||||||
// message and conversation history. Central fix for Issues 1-3, 8-10.
|
|
||||||
// For genuine continuations: anchors to the PRIOR USER TURN (Issues 3/8) and
|
|
||||||
// adds a tail-biased snip from the last assistant reply (Issue 9).
|
|
||||||
// For new topics: blends up to 3 prior user turns for thread continuity (Issue 10).
|
|
||||||
fn build_activation_seed(message: String, hist: String, hist_len: Int) -> String {
|
|
||||||
if hist_len == 0 { return message }
|
|
||||||
let is_cont: Bool = engram_is_continuation(message, hist_len)
|
|
||||||
if is_cont {
|
|
||||||
// Scan back to find the most recent USER turn as topic anchor (Issues 3/8 fix)
|
|
||||||
let prior_user_content: String = ""
|
|
||||||
let scan_idx: Int = hist_len - 1
|
|
||||||
let found_prior: Bool = false
|
|
||||||
while scan_idx >= 0 && !found_prior {
|
|
||||||
let se: String = json_array_get(hist, scan_idx)
|
|
||||||
let se_role: String = json_get(se, "role")
|
|
||||||
let se_content: String = json_get(se, "content")
|
|
||||||
let prior_user_content = if str_eq(se_role, "user") && !found_prior { se_content } else { prior_user_content }
|
|
||||||
let found_prior = if str_eq(se_role, "user") { true } else { found_prior }
|
|
||||||
let scan_idx = scan_idx - 1
|
|
||||||
}
|
|
||||||
// Tail-biased snip from last assistant reply (Issue 9 fix)
|
|
||||||
let last_asst: String = json_array_get(hist, hist_len - 1)
|
|
||||||
let last_asst_role: String = json_get(last_asst, "role")
|
|
||||||
let last_asst_content: String = if str_eq(last_asst_role, "assistant") { json_get(last_asst, "content") } else { "" }
|
|
||||||
let asst_snip: String = if str_eq(last_asst_content, "") { "" } else { topic_snip_from_entry(last_asst_content) }
|
|
||||||
let user_snip: String = if str_len(prior_user_content) > 150 { str_slice(prior_user_content, 0, 150) } else { prior_user_content }
|
|
||||||
// Seed: prior user topic (primary anchor) + assistant tail (context) + current message
|
|
||||||
let s: String = if !str_eq(user_snip, "") {
|
|
||||||
if !str_eq(asst_snip, "") { user_snip + " " + asst_snip + " " + message }
|
|
||||||
else { user_snip + " " + message }
|
|
||||||
} else {
|
|
||||||
if !str_eq(asst_snip, "") { asst_snip + " " + message } else { message }
|
|
||||||
}
|
|
||||||
if str_len(s) > 400 { return str_slice(s, 0, 400) }
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
// Not a continuation: blend with multi-turn user topics for richer seed (Issue 10)
|
|
||||||
let mt: String = multi_turn_topic(hist, hist_len)
|
|
||||||
if str_eq(mt, "") { return message }
|
|
||||||
let b: String = message + " " + mt
|
|
||||||
if str_len(b) > 400 { return str_slice(b, 0, 400) }
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
fn engram_compile(intent: String) -> String {
|
fn engram_compile(intent: String) -> String {
|
||||||
// Issue 1: decompose multi-topic messages into sub-queries.
|
// Issue 1: decompose multi-topic messages into sub-queries.
|
||||||
let topics: String = engram_split_topics(intent)
|
let topics: String = engram_split_topics(intent)
|
||||||
@@ -547,7 +379,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) }
|
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 { "" }
|
||||||
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 has_main: Bool = !str_eq(merged_nodes, "") && !str_eq(merged_nodes, "[]")
|
let has_main: Bool = !str_eq(merged_nodes, "") && !str_eq(merged_nodes, "[]")
|
||||||
let main_part: String = if has_main { merged_nodes } else { scan_part }
|
let main_part: String = if has_main { merged_nodes } else { scan_part }
|
||||||
@@ -604,16 +460,6 @@ fn build_system_prompt(ctx: String) -> String {
|
|||||||
"\n\n[IDENTITY GRAPH — who you are, loaded from your engram]\n" + id_ctx
|
"\n\n[IDENTITY GRAPH — who you are, loaded from your engram]\n" + id_ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// soul_affective_context is loaded at boot by load_identity_context() with BellEvent/
|
|
||||||
// PositiveEvent nodes from last 7 days. Surfaced here so the LLM sees historical
|
|
||||||
// emotional patterns from prior sessions at every turn.
|
|
||||||
let boot_aff_ctx: String = state_get("soul_affective_context")
|
|
||||||
let affective_boot_block: String = if str_eq(boot_aff_ctx, "") {
|
|
||||||
""
|
|
||||||
} else {
|
|
||||||
"\n\n[CROSS-SESSION EMOTIONAL CONTEXT — from prior sessions]\n" + boot_aff_ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
let engram_block: String = if str_eq(ctx, "") {
|
let engram_block: String = if str_eq(ctx, "") {
|
||||||
""
|
""
|
||||||
} else {
|
} else {
|
||||||
@@ -785,15 +631,10 @@ fn handle_chat(body: String) -> String {
|
|||||||
message
|
message
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cross-session affective context: fix Issues 4 and 10.
|
// Cross-session affective context: on session start (no history yet), check engram
|
||||||
// Issue 4: soul_affective_context was computed at boot (soul.el:load_identity_context)
|
// for recent distress signals within 72h and prepend a care directive if found.
|
||||||
// but never consumed here — this block duplicated the search unnecessarily.
|
|
||||||
// Now we read the pre-computed state key and only fall back to a live search when empty.
|
|
||||||
// Issue 10: the live fallback reads created_at (not the never-written "ts" field),
|
|
||||||
// so BellEvent recency filtering now works correctly.
|
|
||||||
let affective_prefix: String = {
|
let affective_prefix: String = {
|
||||||
// Runs every turn. Uses correct BellEvent/PositiveEvent query tags.
|
// Runs every turn. Uses correct BellEvent/PositiveEvent tags.
|
||||||
// Timestamps extracted from embedded ts marker, not created_at.
|
|
||||||
let aff_now_ts: Int = time_now()
|
let aff_now_ts: Int = time_now()
|
||||||
let aff_cutoff: Int = aff_now_ts - 259200
|
let aff_cutoff: Int = aff_now_ts - 259200
|
||||||
let boot_aff: String = state_get("soul_affective_context")
|
let boot_aff: String = state_get("soul_affective_context")
|
||||||
@@ -841,14 +682,10 @@ fn handle_chat(body: String) -> String {
|
|||||||
paff_ts > aff_cutoff
|
paff_ts > aff_cutoff
|
||||||
} else { false }
|
} else { false }
|
||||||
if found_recent_dist {
|
if found_recent_dist {
|
||||||
"[RECENT CONTEXT: User recently expressed significant distress. Monitor for indirect crisis signals and respond with care.]
|
"[RECENT CONTEXT: User recently expressed significant distress. Monitor for indirect crisis signals and respond with care.]\n\n"
|
||||||
|
|
||||||
"
|
|
||||||
} else {
|
} else {
|
||||||
if found_recent_pos {
|
if found_recent_pos {
|
||||||
"[RECENT CONTEXT: User recently shared exciting or joyful news. Acknowledge and celebrate with them when relevant.]
|
"[RECENT CONTEXT: User recently shared exciting or joyful news. Acknowledge and celebrate with them when relevant.]\n\n"
|
||||||
|
|
||||||
"
|
|
||||||
} else { "" }
|
} else { "" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -856,154 +693,100 @@ fn handle_chat(body: String) -> String {
|
|||||||
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)
|
||||||
|
|
||||||
// session_preload_bullets — render up to max_bullets "- <content>" lines from a node array.
|
// Issue 9 fix: add project-specific and session-summary searches to session preload.
|
||||||
// Fix Issue 2: snip_len parameter replaces hardcoded 120 (caller uses 350).
|
// Old hardcoded "user profile" and "in_progress active project" miss project-specific
|
||||||
// Fix Issue 3: max_bullets parameter replaces hardcoded 3/2 caps; loop-driven not unrolled.
|
// nodes stored under names like "Prism" unless those exact words appear in content.
|
||||||
fn session_preload_bullets(nodes: String, max_bullets: Int, snip_len: Int) -> String {
|
|
||||||
if str_eq(nodes, "") { return "" }
|
|
||||||
if str_eq(nodes, "[]") { return "" }
|
|
||||||
let total: Int = json_array_len(nodes)
|
|
||||||
let limit: Int = if max_bullets < total { max_bullets } else { total }
|
|
||||||
let bullets: String = ""
|
|
||||||
let i: Int = 0
|
|
||||||
while i < limit {
|
|
||||||
let node: String = json_array_get(nodes, i)
|
|
||||||
let content: String = json_get(node, "content")
|
|
||||||
let snip: String = if str_len(content) > snip_len { str_slice(content, 0, snip_len) } else { content }
|
|
||||||
let bullets = if str_eq(snip, "") {
|
|
||||||
bullets
|
|
||||||
} else {
|
|
||||||
if str_eq(bullets, "") { "- " + snip } else { bullets + "
|
|
||||||
- " + snip }
|
|
||||||
}
|
|
||||||
let i = i + 1
|
|
||||||
}
|
|
||||||
return bullets
|
|
||||||
}
|
|
||||||
|
|
||||||
// First message of the session: proactively load user profile, active work, and
|
|
||||||
// cross-session continuity context so the soul greets the user with real grounding.
|
|
||||||
// Fix Issue 1: type-targeted queries (Persona, WorkItem) first; broad fallback only
|
|
||||||
// when typed query returns empty. Avoids generic keyword bags that miss typed nodes.
|
|
||||||
// Fix Issue 2: truncation raised from 120 to 350 chars per bullet.
|
|
||||||
// Fix Issue 3: caps raised to 8 profile / 6 work, loop-driven (no hardcoded unrolling).
|
|
||||||
// Fix Issue 5: add continuity search (last session topic via session:emotional-summary).
|
|
||||||
// Fix Issue 6: detect low-info greeting and inject a first-message orientation directive.
|
|
||||||
// Fix Issue 7: when all searches return empty, fall back to pinned identity nodes and log.
|
|
||||||
// Fix Issue 8: preload always fires on first message; greeting detection controls the
|
|
||||||
// orientation directive only (substantive openers still get grounding).
|
|
||||||
let session_preload: String = if hist_len == 0 {
|
let session_preload: String = if hist_len == 0 {
|
||||||
// Issue 6/8: detect greeting vs. substantive opener.
|
let profile_nodes: String = engram_search_json("user profile identity preferences", 5)
|
||||||
let is_greeting: Bool = str_len(message) <= 20
|
let work_nodes: String = engram_search_json("in_progress active project work", 5)
|
||||||
|| str_starts_with(message, "hi")
|
let project_nodes: String = engram_search_json("project status current ongoing active", 5)
|
||||||
|| str_starts_with(message, "hello")
|
let summary_nodes: String = engram_search_json("SessionSummary session:summary previous-session recent", 3)
|
||||||
|| str_starts_with(message, "hey")
|
|
||||||
|
|
||||||
// Issue 1: typed profile query — Persona node_type + soul:persona label first.
|
|
||||||
let profile_nodes_typed: String = engram_search_json("Persona soul:persona identity principal", 8)
|
|
||||||
let profile_ok_typed: Bool = !str_eq(profile_nodes_typed, "") && !str_eq(profile_nodes_typed, "[]")
|
|
||||||
let profile_nodes: String = if profile_ok_typed {
|
|
||||||
profile_nodes_typed
|
|
||||||
} else {
|
|
||||||
engram_search_json("user profile preferences name", 8)
|
|
||||||
}
|
|
||||||
let profile_ok: Bool = !str_eq(profile_nodes, "") && !str_eq(profile_nodes, "[]")
|
let profile_ok: Bool = !str_eq(profile_nodes, "") && !str_eq(profile_nodes, "[]")
|
||||||
|
|
||||||
// Issue 1: typed work query — WorkItem with in_progress label first.
|
|
||||||
let work_nodes_typed: String = engram_search_json("WorkItem status:in_progress active work", 6)
|
|
||||||
let work_ok_typed: Bool = !str_eq(work_nodes_typed, "") && !str_eq(work_nodes_typed, "[]")
|
|
||||||
let work_nodes: String = if work_ok_typed {
|
|
||||||
work_nodes_typed
|
|
||||||
} else {
|
|
||||||
engram_search_json("active project task current in_progress", 6)
|
|
||||||
}
|
|
||||||
let work_ok: Bool = !str_eq(work_nodes, "") && !str_eq(work_nodes, "[]")
|
let work_ok: Bool = !str_eq(work_nodes, "") && !str_eq(work_nodes, "[]")
|
||||||
|
let project_ok: Bool = !str_eq(project_nodes, "") && !str_eq(project_nodes, "[]")
|
||||||
|
let summary_ok: Bool = !str_eq(summary_nodes, "") && !str_eq(summary_nodes, "[]")
|
||||||
|
|
||||||
// Issue 5: cross-session continuity — last session emotional summary or last-session-topic.
|
let profile_bullets: String = if profile_ok {
|
||||||
let continuity_nodes: String = engram_search_json("last-session-topic session:emotional-summary conv:history last session", 3)
|
let pn: Int = json_array_len(profile_nodes)
|
||||||
let continuity_ok: Bool = !str_eq(continuity_nodes, "") && !str_eq(continuity_nodes, "[]")
|
let bullets: String = ""
|
||||||
let continuity_snip: String = if continuity_ok {
|
let bullets = if pn > 0 {
|
||||||
let cn0: String = json_array_get(continuity_nodes, 0)
|
let n0: String = json_array_get(profile_nodes, 0)
|
||||||
let cc: String = json_get(cn0, "content")
|
let c0: String = json_get(n0, "content")
|
||||||
if str_len(cc) > 350 { str_slice(cc, 0, 350) } else { cc }
|
let s0: String = if str_len(c0) > 120 { str_slice(c0, 0, 120) } else { c0 }
|
||||||
|
if str_eq(s0, "") { bullets } else { "- " + s0 }
|
||||||
|
} else { bullets }
|
||||||
|
let bullets = if pn > 1 {
|
||||||
|
let n1: String = json_array_get(profile_nodes, 1)
|
||||||
|
let c1: String = json_get(n1, "content")
|
||||||
|
let s1: String = if str_len(c1) > 120 { str_slice(c1, 0, 120) } else { c1 }
|
||||||
|
if str_eq(s1, "") { bullets } else { bullets + "\n- " + s1 }
|
||||||
|
} else { bullets }
|
||||||
|
let bullets = if pn > 2 {
|
||||||
|
let n2: String = json_array_get(profile_nodes, 2)
|
||||||
|
let c2: String = json_get(n2, "content")
|
||||||
|
let s2: String = if str_len(c2) > 120 { str_slice(c2, 0, 120) } else { c2 }
|
||||||
|
if str_eq(s2, "") { bullets } else { bullets + "\n- " + s2 }
|
||||||
|
} else { bullets }
|
||||||
|
bullets
|
||||||
} else { "" }
|
} else { "" }
|
||||||
|
|
||||||
// Issue 7: fallback to pinned identity nodes when all searches return empty.
|
let work_bullets: String = if work_ok {
|
||||||
let all_empty: Bool = !profile_ok && !work_ok && !continuity_ok
|
let wn: Int = json_array_len(work_nodes)
|
||||||
let fallback_identity: String = if all_empty {
|
let wb: String = ""
|
||||||
let family_node: String = engram_get_node_json("knw-35940684-abc4-42f0-b942-818f66b1f69a")
|
let wb = if wn > 0 {
|
||||||
let origin_node: String = engram_get_node_json("knw-729fc901-8335-44c4-9f3a-b150b4aa0915")
|
let w0: String = json_array_get(work_nodes, 0)
|
||||||
let fam_ok: Bool = !str_eq(family_node, "") && !str_eq(family_node, "null")
|
let wc0: String = json_get(w0, "content")
|
||||||
let orig_ok: Bool = !str_eq(origin_node, "") && !str_eq(origin_node, "null")
|
let ws0: String = if str_len(wc0) > 120 { str_slice(wc0, 0, 120) } else { wc0 }
|
||||||
let fam_content: String = if fam_ok { json_get(family_node, "content") } else { "" }
|
if str_eq(ws0, "") { wb } else { "- " + ws0 }
|
||||||
let orig_content: String = if orig_ok { json_get(origin_node, "content") } else { "" }
|
} else { wb }
|
||||||
let fam_snip: String = if str_len(fam_content) > 350 { str_slice(fam_content, 0, 350) } else { fam_content }
|
let wb = if wn > 1 {
|
||||||
let orig_snip: String = if str_len(orig_content) > 350 { str_slice(orig_content, 0, 350) } else { orig_content }
|
let w1: String = json_array_get(work_nodes, 1)
|
||||||
let fb: String = if fam_ok {
|
let wc1: String = json_get(w1, "content")
|
||||||
if orig_ok { "- " + fam_snip + "
|
let ws1: String = if str_len(wc1) > 120 { str_slice(wc1, 0, 120) } else { wc1 }
|
||||||
- " + orig_snip } else { "- " + fam_snip }
|
if str_eq(ws1, "") { wb } else { wb + "\n- " + ws1 }
|
||||||
} else {
|
} else { wb }
|
||||||
if orig_ok { "- " + orig_snip } else { "" }
|
wb
|
||||||
}
|
|
||||||
if str_eq(fb, "") {
|
|
||||||
println("[chat] session_preload: all engram searches empty and pinned nodes missing — grounding context unavailable")
|
|
||||||
} else {
|
|
||||||
println("[chat] session_preload: all typed/broad searches empty — using pinned identity nodes as fallback")
|
|
||||||
}
|
|
||||||
fb
|
|
||||||
} else { "" }
|
} else { "" }
|
||||||
|
|
||||||
// Issue 2 + 3: render bullets with raised caps and 350-char snip.
|
let project_bullets: String = if project_ok {
|
||||||
let profile_bullets: String = session_preload_bullets(profile_nodes, 8, 350)
|
let prn: Int = json_array_len(project_nodes)
|
||||||
let work_bullets: String = session_preload_bullets(work_nodes, 6, 350)
|
let pb: String = ""
|
||||||
|
let pb = if prn > 0 {
|
||||||
let has_profile: Bool = !str_eq(profile_bullets, "")
|
let pr0: String = json_array_get(project_nodes, 0)
|
||||||
let has_work: Bool = !str_eq(work_bullets, "")
|
let prc0: String = json_get(pr0, "content")
|
||||||
let has_continuity: Bool = !str_eq(continuity_snip, "")
|
let ps0: String = if str_len(prc0) > 120 { str_slice(prc0, 0, 120) } else { prc0 }
|
||||||
let has_fallback: Bool = !str_eq(fallback_identity, "")
|
if str_eq(ps0, "") { pb } else { "- " + ps0 }
|
||||||
|
} else { pb }
|
||||||
// Issue 6: orient the soul on greeting openers to ask a check-in question first.
|
let pb = if prn > 1 {
|
||||||
let continuity_directive: String = if is_greeting && has_continuity {
|
let pr1: String = json_array_get(project_nodes, 1)
|
||||||
"[SESSION START — FIRST TURN] New session. The user sent a short greeting. Orient yourself: acknowledge you are present and ask what they would like to work on or continue. Do not recite the context below — use it only for orientation.
|
let prc1: String = json_get(pr1, "content")
|
||||||
"
|
let ps1: String = if str_len(prc1) > 120 { str_slice(prc1, 0, 120) } else { prc1 }
|
||||||
|
if str_eq(ps1, "") { pb } else { pb + "\n- " + ps1 }
|
||||||
|
} else { pb }
|
||||||
|
pb
|
||||||
} else { "" }
|
} else { "" }
|
||||||
|
|
||||||
let preload: String = if has_profile || has_work || has_continuity || has_fallback {
|
let summary_bullet: String = if summary_ok {
|
||||||
let directive_part: String = continuity_directive
|
let sn0: String = json_array_get(summary_nodes, 0)
|
||||||
let profile_part: String = if has_profile {
|
let sc0: String = json_get(sn0, "content")
|
||||||
"[USER CONTEXT — from memory]
|
let ss0: String = if str_len(sc0) > 200 { str_slice(sc0, 0, 200) } else { sc0 }
|
||||||
" + profile_bullets + "
|
if str_eq(ss0, "") { "" } else { "- " + ss0 }
|
||||||
|
} else { "" }
|
||||||
|
|
||||||
"
|
let hp: Bool = !str_eq(profile_bullets, "")
|
||||||
} else { "" }
|
let hw: Bool = !str_eq(work_bullets, "")
|
||||||
let work_part: String = if has_work {
|
let hpr: Bool = !str_eq(project_bullets, "")
|
||||||
"[ACTIVE WORK — from memory]
|
let hs: Bool = !str_eq(summary_bullet, "")
|
||||||
" + work_bullets + "
|
let preload: String = if hp || hw || hpr || hs {
|
||||||
|
let sec_p: String = if hp { "[USER CONTEXT — from memory]\n" + profile_bullets } else { "" }
|
||||||
"
|
let sec_w: String = if hw { "[ACTIVE WORK — from memory]\n" + work_bullets } else { "" }
|
||||||
} else { "" }
|
let sec_pr: String = if hpr { "[PROJECTS — from memory]\n" + project_bullets } else { "" }
|
||||||
let continuity_part: String = if has_continuity {
|
let sec_s: String = if hs { "[PREVIOUS SESSION — from memory]\n" + summary_bullet } else { "" }
|
||||||
"[CONTINUING FROM LAST SESSION]
|
let sep1: String = if hp && (hw || hpr || hs) { "\n\n" } else { "" }
|
||||||
" + continuity_snip + "
|
let sep2: String = if hw && (hpr || hs) { "\n\n" } else { "" }
|
||||||
|
let sep3: String = if hpr && hs { "\n\n" } else { "" }
|
||||||
"
|
"\n\n" + sec_p + sep1 + sec_w + sep2 + sec_pr + sep3 + sec_s
|
||||||
} else { "" }
|
|
||||||
let fallback_part: String = if has_fallback && !has_profile && !has_work {
|
|
||||||
"[IDENTITY CONTEXT — from memory]
|
|
||||||
" + fallback_identity + "
|
|
||||||
|
|
||||||
"
|
|
||||||
} else { "" }
|
|
||||||
let body: String = directive_part + profile_part + work_part + continuity_part + fallback_part
|
|
||||||
let body_len: Int = str_len(body)
|
|
||||||
let trimmed_body: String = if body_len > 2 && str_eq(str_slice(body, body_len - 2, body_len), "
|
|
||||||
|
|
||||||
") {
|
|
||||||
str_slice(body, 0, body_len - 2)
|
|
||||||
} else { body }
|
|
||||||
"
|
|
||||||
|
|
||||||
" + trimmed_body
|
|
||||||
} else { "" }
|
} else { "" }
|
||||||
preload
|
preload
|
||||||
} else { "" }
|
} else { "" }
|
||||||
@@ -1531,53 +1314,7 @@ fn handle_chat_agentic(body: String) -> String {
|
|||||||
|
|
||||||
let ctx: String = engram_compile(ag_seed)
|
let ctx: String = engram_compile(ag_seed)
|
||||||
let identity: String = state_get("soul_identity")
|
let identity: String = state_get("soul_identity")
|
||||||
|
let system: String = identity + " You have access to tools: read files, write files, browse the web, search your memory, run commands. Use them when they add genuine value. Be direct.\n\n" + ctx
|
||||||
// Issue 9: agentic first-message session preload — mirrors handle_chat grounding.
|
|
||||||
let ag_session_preload: String = if agentic_hist_len == 0 {
|
|
||||||
let ag_profile_nodes: String = engram_search_json("Persona soul:persona identity principal", 8)
|
|
||||||
let ag_profile_ok: Bool = !str_eq(ag_profile_nodes, "") && !str_eq(ag_profile_nodes, "[]")
|
|
||||||
let ag_profile_nodes2: String = if ag_profile_ok { ag_profile_nodes } else {
|
|
||||||
engram_search_json("user profile preferences name", 8)
|
|
||||||
}
|
|
||||||
let ag_work_nodes: String = engram_search_json("WorkItem status:in_progress active work", 6)
|
|
||||||
let ag_work_ok: Bool = !str_eq(ag_work_nodes, "") && !str_eq(ag_work_nodes, "[]")
|
|
||||||
let ag_work_nodes2: String = if ag_work_ok { ag_work_nodes } else {
|
|
||||||
engram_search_json("active project task current in_progress", 6)
|
|
||||||
}
|
|
||||||
let ag_continuity_nodes: String = engram_search_json("last-session-topic session:emotional-summary conv:history last session", 3)
|
|
||||||
let ag_continuity_ok: Bool = !str_eq(ag_continuity_nodes, "") && !str_eq(ag_continuity_nodes, "[]")
|
|
||||||
let ag_continuity_snip: String = if ag_continuity_ok {
|
|
||||||
let acn0: String = json_array_get(ag_continuity_nodes, 0)
|
|
||||||
let acc: String = json_get(acn0, "content")
|
|
||||||
if str_len(acc) > 350 { str_slice(acc, 0, 350) } else { acc }
|
|
||||||
} else { "" }
|
|
||||||
let ag_profile_bullets: String = session_preload_bullets(ag_profile_nodes2, 8, 350)
|
|
||||||
let ag_work_bullets: String = session_preload_bullets(ag_work_nodes2, 6, 350)
|
|
||||||
let ag_has_profile: Bool = !str_eq(ag_profile_bullets, "")
|
|
||||||
let ag_has_work: Bool = !str_eq(ag_work_bullets, "")
|
|
||||||
let ag_has_cont: Bool = !str_eq(ag_continuity_snip, "")
|
|
||||||
if ag_has_profile || ag_has_work || ag_has_cont {
|
|
||||||
let p: String = if ag_has_profile { "[USER CONTEXT — from memory]
|
|
||||||
" + ag_profile_bullets + "
|
|
||||||
|
|
||||||
" } else { "" }
|
|
||||||
let w: String = if ag_has_work { "[ACTIVE WORK — from memory]
|
|
||||||
" + ag_work_bullets + "
|
|
||||||
|
|
||||||
" } else { "" }
|
|
||||||
let c: String = if ag_has_cont { "[CONTINUING FROM LAST SESSION]
|
|
||||||
" + ag_continuity_snip + "
|
|
||||||
|
|
||||||
" } else { "" }
|
|
||||||
"
|
|
||||||
|
|
||||||
" + p + w + c
|
|
||||||
} else { "" }
|
|
||||||
} else { "" }
|
|
||||||
|
|
||||||
let system: String = identity + " You have access to tools: read files, write files, browse the web, search your memory, run commands. Use them when they add genuine value. Be direct.
|
|
||||||
|
|
||||||
" + ctx + ag_session_preload
|
|
||||||
|
|
||||||
let api_key: String = agentic_api_key()
|
let api_key: String = agentic_api_key()
|
||||||
let tools_json: String = agentic_tools_all()
|
let tools_json: String = agentic_tools_all()
|
||||||
@@ -1965,8 +1702,7 @@ fn handle_dharma_room_turn(body: String) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The soul's own memories, activated by what it's reading — not injected.
|
// The soul's own memories, activated by what it's reading — not injected.
|
||||||
// Issue 6 fix: distill_transcript() extracts salient tail+question from full transcript
|
let engram_ctx: String = engram_compile(transcript)
|
||||||
let engram_ctx: String = engram_compile(distill_transcript(transcript))
|
|
||||||
let system_prompt: String = if str_eq(engram_ctx, "") {
|
let system_prompt: String = if str_eq(engram_ctx, "") {
|
||||||
identity
|
identity
|
||||||
} else {
|
} else {
|
||||||
@@ -2018,8 +1754,7 @@ fn handle_dharma_room_turn_agentic(body: String) -> String {
|
|||||||
return "{\"error\":\"transcript is required\",\"response\":\"\",\"cgi_id\":\"" + cgi_id + "\"}"
|
return "{\"error\":\"transcript is required\",\"response\":\"\",\"cgi_id\":\"" + cgi_id + "\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue 6 fix: distill_transcript() extracts salient tail+question from full transcript
|
let ctx: String = engram_compile(transcript)
|
||||||
let ctx: String = engram_compile(distill_transcript(transcript))
|
|
||||||
let system: String = identity + " You have access to tools: read files, write files, browse the web, search your memory, run commands. Use them when they add genuine value. Be direct and stay in character.\n\n" + ctx
|
let system: String = identity + " You have access to tools: read files, write files, browse the web, search your memory, run commands. Use them when they add genuine value. Be direct and stay in character.\n\n" + ctx
|
||||||
|
|
||||||
let api_key: String = agentic_api_key()
|
let api_key: String = agentic_api_key()
|
||||||
@@ -2081,7 +1816,6 @@ 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")
|
||||||
// Positive emotion detection mirrors distress detection.
|
|
||||||
let positive_level: String = safety_detect_positive_level(message)
|
let positive_level: String = safety_detect_positive_level(message)
|
||||||
let is_positive: Bool = !str_eq(positive_level, "none")
|
let is_positive: Bool = !str_eq(positive_level, "none")
|
||||||
|
|
||||||
|
|||||||
+8
-4
@@ -24,19 +24,23 @@ ENGRAM_DATA_DIR="$ENGRAM_DATA_DIR" \
|
|||||||
|
|
||||||
ENGRAM_PID=$!
|
ENGRAM_PID=$!
|
||||||
|
|
||||||
# Wait for engram to become healthy (up to 30s)
|
# Wait for engram to become healthy (up to 60s; GKE Autopilot cold starts can be slow)
|
||||||
echo "[entrypoint] waiting for engram..."
|
echo "[entrypoint] waiting for engram..."
|
||||||
TRIES=0
|
TRIES=0
|
||||||
until curl -sf "$ENGRAM_HEALTH_URL" > /dev/null 2>&1; do
|
until curl -sf "$ENGRAM_HEALTH_URL" > /dev/null 2>&1; do
|
||||||
TRIES=$((TRIES + 1))
|
TRIES=$((TRIES + 1))
|
||||||
if [ "$TRIES" -ge 30 ]; then
|
if [ "$TRIES" -ge 60 ]; then
|
||||||
echo "[entrypoint] ERROR: engram did not become healthy after 30s" >&2
|
echo "[entrypoint] ERROR: engram did not become healthy after 60s" >&2
|
||||||
kill "$ENGRAM_PID" 2>/dev/null || true
|
kill "$ENGRAM_PID" 2>/dev/null || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
echo "[entrypoint] engram ready"
|
echo "[entrypoint] engram ready after ${TRIES}s"
|
||||||
|
|
||||||
|
# Tune EL HTTP runtime: reduce per-call timeout 60s->10s, connect timeout 3s.
|
||||||
|
export EL_HTTP_TIMEOUT_MS="${EL_HTTP_TIMEOUT_MS:-10000}"
|
||||||
|
export EL_HTTP_CONNECT_TIMEOUT_MS="${EL_HTTP_CONNECT_TIMEOUT_MS:-3000}"
|
||||||
|
|
||||||
# Start soul — it takes over as PID 1's foreground process.
|
# Start soul — it takes over as PID 1's foreground process.
|
||||||
# SOUL_ENGRAM_PATH must NOT be set; ENGRAM_URL triggers HTTP mode.
|
# SOUL_ENGRAM_PATH must NOT be set; ENGRAM_URL triggers HTTP mode.
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
|
|
||||||
// imprint_current — returns the active imprint ID from state.
|
// imprint_current — returns the active imprint ID from state.
|
||||||
// Falls back to "base" (bare Neuron, no suit) when nothing is loaded.
|
// Falls back to "base" (bare Neuron, no suit) when nothing is loaded.
|
||||||
|
//
|
||||||
|
// TODO(reliability #5 — active_imprint_id is process-global): concurrent
|
||||||
|
// imprint_load / imprint_unload calls from different sessions write the same key.
|
||||||
|
// Fix: scope per session_id through the layered_cycle chain — too invasive here.
|
||||||
fn imprint_current() -> String {
|
fn imprint_current() -> String {
|
||||||
let id: String = state_get("active_imprint_id")
|
let id: String = state_get("active_imprint_id")
|
||||||
return if str_eq(id, "") { "base" } else { id }
|
return if str_eq(id, "") { "base" } else { id }
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ fn mem_consolidate() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn mem_save(path: String) -> Void {
|
fn mem_save(path: String) -> Void {
|
||||||
engram_save(path)
|
let save_result: String = engram_save(path)
|
||||||
|
if str_eq(save_result, "") {
|
||||||
|
println("[memory] mem_save: engram_save failed for " + path + " — snapshot may be incomplete")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mem_load(path: String) -> Void {
|
fn mem_load(path: String) -> Void {
|
||||||
@@ -76,11 +79,14 @@ fn mem_boot_count_inc() -> Int {
|
|||||||
let next: Int = current + 1
|
let next: Int = current + 1
|
||||||
let content: String = "soul:boot_count:" + int_to_str(next)
|
let content: String = "soul:boot_count:" + int_to_str(next)
|
||||||
let tags: String = "[\"soul-meta\",\"boot-counter\"]"
|
let tags: String = "[\"soul-meta\",\"boot-counter\"]"
|
||||||
let discard: String = engram_node_full(
|
let boot_node_id: String = engram_node_full(
|
||||||
content, "Memory", "soul:boot_count",
|
content, "Memory", "soul:boot_count",
|
||||||
el_from_float(0.9), el_from_float(0.9), el_from_float(1.0),
|
el_from_float(0.9), el_from_float(0.9), el_from_float(1.0),
|
||||||
"Canonical", tags
|
"Canonical", tags
|
||||||
)
|
)
|
||||||
|
if str_eq(boot_node_id, "") {
|
||||||
|
println("[memory] mem_boot_count_inc: engram write failed — boot counter node lost (count=" + int_to_str(next) + ")")
|
||||||
|
}
|
||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -400,6 +400,7 @@ fn handle_api_log_state_event(body: String) -> String {
|
|||||||
let id: String = engram_node_full(parts, "InternalStateEvent", "state-event:manual",
|
let id: String = engram_node_full(parts, "InternalStateEvent", "state-event:manual",
|
||||||
el_from_float(0.85), el_from_float(0.85), el_from_float(0.9),
|
el_from_float(0.85), el_from_float(0.85), el_from_float(0.9),
|
||||||
"Episodic", tags)
|
"Episodic", tags)
|
||||||
|
if !api_persisted(id) { return api_not_persisted(id) }
|
||||||
return "{\"ok\":true,\"id\":\"" + id + "\",\"boot\":\"" + boot + "\"}"
|
return "{\"ok\":true,\"id\":\"" + id + "\",\"boot\":\"" + boot + "\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,6 +453,7 @@ fn handle_api_tune_config(body: String) -> String {
|
|||||||
let id: String = engram_node_full(content, "ConfigEntry", key,
|
let id: String = engram_node_full(content, "ConfigEntry", key,
|
||||||
el_from_float(0.85), el_from_float(0.85), el_from_float(0.9),
|
el_from_float(0.85), el_from_float(0.85), el_from_float(0.9),
|
||||||
"Canonical", tags)
|
"Canonical", tags)
|
||||||
|
if !api_persisted(id) { return api_not_persisted(id) }
|
||||||
return "{\"ok\":true,\"key\":\"" + key + "\",\"value\":\"" + value + "\",\"id\":\"" + id + "\"}"
|
return "{\"ok\":true,\"key\":\"" + key + "\",\"value\":\"" + value + "\",\"id\":\"" + id + "\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,17 +653,23 @@ fn handle_api_consolidate(body: String) -> String {
|
|||||||
let summary: String = json_get(body, "summary")
|
let summary: String = json_get(body, "summary")
|
||||||
let snap: String = state_get("soul_snapshot_path")
|
let snap: String = state_get("soul_snapshot_path")
|
||||||
if !str_eq(snap, "") {
|
if !str_eq(snap, "") {
|
||||||
engram_save(snap)
|
let save_result: String = engram_save(snap)
|
||||||
|
if str_eq(save_result, "") {
|
||||||
|
println("[api] consolidate: engram_save failed for " + snap + " — snapshot may be out of sync")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !str_eq(summary, "") {
|
if !str_eq(summary, "") {
|
||||||
let safe_summary: String = str_replace(summary, "\"", "'")
|
let safe_summary: String = str_replace(summary, "\"", "'")
|
||||||
let tags: String = "[\"SessionSummary\",\"consolidate\"]"
|
let tags: String = "[\"SessionSummary\",\"consolidate\"]"
|
||||||
let discard: String = engram_node_full(
|
let summary_id: String = engram_node_full(
|
||||||
"[session-summary] " + safe_summary,
|
"[session-summary] " + safe_summary,
|
||||||
"SessionSummary", "session:summary",
|
"SessionSummary", "session:summary",
|
||||||
el_from_float(0.7), el_from_float(0.7), el_from_float(0.9),
|
el_from_float(0.7), el_from_float(0.7), el_from_float(0.9),
|
||||||
"Episodic", tags
|
"Episodic", tags
|
||||||
)
|
)
|
||||||
|
if str_eq(summary_id, "") {
|
||||||
|
println("[api] consolidate: session summary engram write failed — summary node lost")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return "{\"ok\":true,\"snapshot\":\"" + snap + "\"}"
|
return "{\"ok\":true,\"snapshot\":\"" + snap + "\"}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -367,6 +367,9 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
|||||||
return engram_scan_nodes_json(9999, 0)
|
return engram_scan_nodes_json(9999, 0)
|
||||||
}
|
}
|
||||||
if str_eq(clean, "/api/graph/edges") {
|
if str_eq(clean, "/api/graph/edges") {
|
||||||
|
// TODO(reliability #8): engram_save races with awareness loop mem_save().
|
||||||
|
// Both now use atomic write-to-temp+rename (el_runtime.c). Serialised
|
||||||
|
// by engram_global_mu. Future: add engram_edges_json() builtin.
|
||||||
let snap_path: String = env("HOME") + "/.neuron/engram/snapshot.json"
|
let snap_path: String = env("HOME") + "/.neuron/engram/snapshot.json"
|
||||||
engram_save(snap_path)
|
engram_save(snap_path)
|
||||||
let snap: String = fs_read(snap_path)
|
let snap: String = fs_read(snap_path)
|
||||||
|
|||||||
@@ -144,7 +144,8 @@ fn safety_screen(input: String, history: String) -> String {
|
|||||||
if score >= soft {
|
if score >= soft {
|
||||||
let summary: String = str_slice(input, 0, 80)
|
let summary: String = str_slice(input, 0, 80)
|
||||||
let discard: String = safety_log_bell("soft", "wellbeing check needed", summary)
|
let discard: String = safety_log_bell("soft", "wellbeing check needed", summary)
|
||||||
// ISSUE 7: also escape tab chars to prevent JSON envelope corruption.
|
// ISSUE 7 fix: escape tab chars in addition to backslash/quote/newline/CR.
|
||||||
|
// A tab in user input corrupts the JSON envelope and causes json_get to misparse.
|
||||||
let e1: String = str_replace(input, "\\", "\\\\")
|
let e1: String = str_replace(input, "\\", "\\\\")
|
||||||
let e2: String = str_replace(e1, "\"", "\\\"")
|
let e2: String = str_replace(e1, "\"", "\\\"")
|
||||||
let e3: String = str_replace(e2, "\n", "\\n")
|
let e3: String = str_replace(e2, "\n", "\\n")
|
||||||
@@ -153,7 +154,7 @@ fn safety_screen(input: String, history: String) -> String {
|
|||||||
return "{\"action\":\"soft_bell\",\"reason\":\"wellbeing check needed\",\"content\":\"" + safe_input + "\"}"
|
return "{\"action\":\"soft_bell\",\"reason\":\"wellbeing check needed\",\"content\":\"" + safe_input + "\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ISSUE 7: also escape tab chars (see soft_bell branch above).
|
// ISSUE 7 fix: escape tab chars (see soft_bell branch above for rationale).
|
||||||
let e1: String = str_replace(input, "\\", "\\\\")
|
let e1: String = str_replace(input, "\\", "\\\\")
|
||||||
let e2: String = str_replace(e1, "\"", "\\\"")
|
let e2: String = str_replace(e1, "\"", "\\\"")
|
||||||
let e3: String = str_replace(e2, "\n", "\\n")
|
let e3: String = str_replace(e2, "\n", "\\n")
|
||||||
@@ -199,7 +200,10 @@ fn safety_validate(output: String, action: String) -> String {
|
|||||||
fn safety_log_bell(level: String, reason: String, input_summary: String) -> String {
|
fn safety_log_bell(level: String, reason: String, input_summary: String) -> String {
|
||||||
let content: String = "BELL:" + level + " | " + reason + " | summary:" + input_summary
|
let content: String = "BELL:" + level + " | " + reason + " | summary:" + input_summary
|
||||||
let tags: String = "[\"safety\",\"bell\",\"bell:" + level + "\"]"
|
let tags: String = "[\"safety\",\"bell\",\"bell:" + level + "\"]"
|
||||||
// ISSUE 2: fallback log when engram write fails silently.
|
// ISSUE 2 fix: if engram_node_full returns empty the write silently failed.
|
||||||
|
// Emit a fallback println so the bell event leaves at least a log trace even
|
||||||
|
// when engram is degraded. This does not replace engram persistence -- it is a
|
||||||
|
// last-resort audit trail when the primary write cannot be confirmed.
|
||||||
let node_id: String = engram_node_full(
|
let node_id: String = engram_node_full(
|
||||||
content,
|
content,
|
||||||
"BellEvent",
|
"BellEvent",
|
||||||
@@ -211,7 +215,7 @@ fn safety_log_bell(level: String, reason: String, input_summary: String) -> Stri
|
|||||||
tags
|
tags
|
||||||
)
|
)
|
||||||
if str_eq(node_id, "") {
|
if str_eq(node_id, "") {
|
||||||
println("[safety] WARN: bell engram write failed -- " + content)
|
println("[safety] WARN: bell event engram write failed -- fallback log: " + content)
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -240,13 +244,20 @@ 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\",\"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.
|
// ISSUE 5 TODO: phrase lists are rebuilt from JSON literals on every call.
|
||||||
// json_array_len of malformed input returns 0, silently skipping all checks.
|
// safety_any_match and safety_count_match loop over json_array_get on every invocation.
|
||||||
// Caching requires language-level static const arrays -- not in current EL.
|
// A compiled/cached representation would reduce per-message overhead and also guard against
|
||||||
// Migrate to const arrays when EL gains that feature.
|
// malformed phrase JSON (json_array_len of malformed input returns 0, silently skipping all checks).
|
||||||
|
// Caching requires language-level static const arrays -- not available in current EL.
|
||||||
|
// When EL gains module-level const arrays, migrate phrase lists to that form.
|
||||||
|
//
|
||||||
|
// ISSUE 5 TODO: phrase lists are rebuilt from JSON literals on every call to
|
||||||
|
// safety_any_match / safety_count_match. json_array_len of a malformed string
|
||||||
|
// returns 0, silently skipping all checks. Caching requires language-level static
|
||||||
|
// const arrays (not available in current EL). Migrate when EL gains that feature.
|
||||||
// ── Matching helpers (single loops only — el escapes while-body mutation via
|
// ── Matching helpers (single loops only — el escapes while-body mutation via
|
||||||
// top-level let rebinds; nested loops would not advance) ────────────────────
|
// top-level let rebinds; nested loops would not advance) ────────────────────
|
||||||
|
|
||||||
@@ -284,6 +295,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
|
// 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())
|
||||||
|
|||||||
+4
-32
@@ -104,6 +104,8 @@ fn session_create(body: String) -> String {
|
|||||||
// Newest sessions first (prepend).
|
// Newest sessions first (prepend).
|
||||||
// TODO #4: index update is read-modify-write — two concurrent session_create
|
// TODO #4: index update is read-modify-write — two concurrent session_create
|
||||||
// calls can lose one entry. EL has no CAS primitive; fix requires runtime support.
|
// calls can lose one entry. EL has no CAS primitive; fix requires runtime support.
|
||||||
|
// TODO(reliability #2): session_index RMW is non-atomic. Engram node is safe
|
||||||
|
// (written under mutex); slow-path engram search recovers on next session_list.
|
||||||
let existing_idx: String = state_get("session_index")
|
let existing_idx: String = state_get("session_index")
|
||||||
let idx_entry: String = "{\"id\":\"" + id + "\",\"title\":\"" + json_safe(title) + "\",\"folder\":\"" + json_safe(folder) + "\",\"created_at\":" + int_to_str(ts) + ",\"updated_at\":" + int_to_str(ts) + ",\"last_message\":\"\"}"
|
let idx_entry: String = "{\"id\":\"" + id + "\",\"title\":\"" + json_safe(title) + "\",\"folder\":\"" + json_safe(folder) + "\",\"created_at\":" + int_to_str(ts) + ",\"updated_at\":" + int_to_str(ts) + ",\"last_message\":\"\"}"
|
||||||
let new_idx: String = if str_eq(existing_idx, "") {
|
let new_idx: String = if str_eq(existing_idx, "") {
|
||||||
@@ -440,6 +442,8 @@ fn session_hist_save(session_id: String, hist: String) -> Void {
|
|||||||
}
|
}
|
||||||
let oi = oi + 1
|
let oi = oi + 1
|
||||||
}
|
}
|
||||||
|
// TODO(reliability #7): delete-then-insert is not atomic — concurrent saves for the
|
||||||
|
// same session can produce orphan history nodes. State is primary truth; engram fallback.
|
||||||
let tags: String = "[\"session\",\"session-history\",\"Conversation\"]"
|
let tags: String = "[\"session\",\"session-history\",\"Conversation\"]"
|
||||||
let discard: String = engram_node_full(
|
let discard: String = engram_node_full(
|
||||||
hist, "Conversation", "session:messages:" + session_id,
|
hist, "Conversation", "session:messages:" + session_id,
|
||||||
@@ -488,38 +492,6 @@ fn session_hist_save(session_id: String, hist: String) -> Void {
|
|||||||
state_set(summary_written_key, "1")
|
state_set(summary_written_key, "1")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue 5 fix: write a last-session-topic Conversation node so future sessions can
|
|
||||||
// find the most recent session's topic via engram search. This enables cross-session
|
|
||||||
// continuity — chat.el searches for "last-session-topic" and shows a [CONTINUING FROM
|
|
||||||
// LAST SESSION] section on the first message of a new session.
|
|
||||||
let hist_arr_len: Int = if str_eq(hist, "") { 0 } else { json_array_len(hist) }
|
|
||||||
if hist_arr_len >= 2 {
|
|
||||||
let last_entry: String = json_array_get(hist, hist_arr_len - 1)
|
|
||||||
let last_role: String = json_get(last_entry, "role")
|
|
||||||
let last_content: String = json_get(last_entry, "content")
|
|
||||||
let topic_snip: String = if str_len(last_content) > 200 { str_slice(last_content, 0, 200) } else { last_content }
|
|
||||||
let safe_topic: String = str_replace(topic_snip, """, "'")
|
|
||||||
let ts_now: String = int_to_str(time_now())
|
|
||||||
let topic_content: String = "last-session-topic | ts:" + ts_now + " | session:" + session_id + " | topic:" + safe_topic
|
|
||||||
let topic_tags: String = "["last-session-topic","conv:history","Conversation","session:topic"]"
|
|
||||||
let topic_label: String = "last-session-topic:" + session_id
|
|
||||||
// Delete old last-session-topic node for this session before writing fresh
|
|
||||||
let old_topic: String = engram_search_json("last-session-topic:" + session_id, 2)
|
|
||||||
let ot_len: Int = if str_eq(old_topic, "") { 0 } else { json_array_len(old_topic) }
|
|
||||||
let oti: Int = 0
|
|
||||||
while oti < ot_len {
|
|
||||||
let ot_node: String = json_array_get(old_topic, oti)
|
|
||||||
let ot_id: String = json_get(ot_node, "id")
|
|
||||||
if !str_eq(ot_id, "") { engram_forget(ot_id) }
|
|
||||||
let oti = oti + 1
|
|
||||||
}
|
|
||||||
let discard_topic: String = engram_node_full(
|
|
||||||
topic_content, "Conversation", topic_label,
|
|
||||||
el_from_float(0.7), el_from_float(0.7), el_from_float(0.9),
|
|
||||||
"Episodic", topic_tags
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// session_update_meta_timestamp — update the updated_at field in the session:meta node.
|
// session_update_meta_timestamp — update the updated_at field in the session:meta node.
|
||||||
|
|||||||
@@ -163,10 +163,7 @@ fn load_identity_context() -> Void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cross-session affective context: load recent BellEvent nodes (distress) and
|
// Cross-session affective context: load BellEvent and PositiveEvent nodes from last 7 days.
|
||||||
// PositiveEvent nodes (joy/success) from the last 7 days. Stored in state as
|
|
||||||
// "soul_affective_context" for build_system_prompt to consume. Uses embedded
|
|
||||||
// " | ts:NNNNN" marker for recency filtering (created_at is unreliable).
|
|
||||||
let aff_now: Int = time_now()
|
let aff_now: Int = time_now()
|
||||||
let aff_7d: Int = aff_now - 604800
|
let aff_7d: Int = aff_now - 604800
|
||||||
let bell_raw: String = engram_search_json("bell:soft bell:hard BellEvent affective", 3)
|
let bell_raw: String = engram_search_json("bell:soft bell:hard BellEvent affective", 3)
|
||||||
@@ -174,9 +171,9 @@ fn load_identity_context() -> Void {
|
|||||||
let aff_ctx: String = ""
|
let aff_ctx: String = ""
|
||||||
let aff_ctx = if bell_aff_ok {
|
let aff_ctx = if bell_aff_ok {
|
||||||
let bn_total: Int = json_array_len(bell_raw)
|
let bn_total: Int = json_array_len(bell_raw)
|
||||||
let result: String = ""
|
let bacc: String = ""
|
||||||
let bi: Int = 0
|
let bi: Int = 0
|
||||||
let result = while bi < bn_total {
|
let bacc = while bi < bn_total {
|
||||||
let bn: String = json_array_get(bell_raw, bi)
|
let bn: String = json_array_get(bell_raw, bi)
|
||||||
let bn_c: String = json_get(bn, "content")
|
let bn_c: String = json_get(bn, "content")
|
||||||
let bm: String = " | ts:"
|
let bm: String = " | ts:"
|
||||||
@@ -192,21 +189,21 @@ fn load_identity_context() -> Void {
|
|||||||
}
|
}
|
||||||
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) }
|
||||||
let snip: String = if str_len(bn_c) > 200 { str_slice(bn_c, 0, 200) } else { bn_c }
|
let snip: String = if str_len(bn_c) > 200 { str_slice(bn_c, 0, 200) } else { bn_c }
|
||||||
let result = if bn_ts >= aff_7d && !str_eq(snip, "") {
|
let bacc = if bn_ts >= aff_7d && !str_eq(snip, "") {
|
||||||
if str_eq(result, "") { snip } else { result + "\n" + snip }
|
if str_eq(bacc, "") { snip } else { bacc + "\n" + snip }
|
||||||
} else { result }
|
} else { bacc }
|
||||||
let bi = bi + 1
|
let bi = bi + 1
|
||||||
result
|
bacc
|
||||||
}
|
}
|
||||||
result
|
bacc
|
||||||
} else { "" }
|
} else { "" }
|
||||||
let pos_raw: String = engram_search_json("PositiveEvent joy:high joy:low affective", 3)
|
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 pos_aff_ok: Bool = !str_eq(pos_raw, "") && !str_eq(pos_raw, "[]")
|
||||||
let aff_ctx = if pos_aff_ok {
|
let aff_ctx = if pos_aff_ok {
|
||||||
let pn_total: Int = json_array_len(pos_raw)
|
let pn_total: Int = json_array_len(pos_raw)
|
||||||
let presult: String = aff_ctx
|
let pacc: String = aff_ctx
|
||||||
let pi: Int = 0
|
let pi: Int = 0
|
||||||
let presult = while pi < pn_total {
|
let pacc = while pi < pn_total {
|
||||||
let pn: String = json_array_get(pos_raw, pi)
|
let pn: String = json_array_get(pos_raw, pi)
|
||||||
let pn_c: String = json_get(pn, "content")
|
let pn_c: String = json_get(pn, "content")
|
||||||
let pm: String = " | ts:"
|
let pm: String = " | ts:"
|
||||||
@@ -222,46 +219,17 @@ fn load_identity_context() -> Void {
|
|||||||
}
|
}
|
||||||
let pn_ts: Int = if str_eq(pn_ts_raw, "") { 0 } else { str_to_int(pn_ts_raw) }
|
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 psnip: String = if str_len(pn_c) > 200 { str_slice(pn_c, 0, 200) } else { pn_c }
|
||||||
let presult = if pn_ts >= aff_7d && !str_eq(psnip, "") {
|
let pacc = if pn_ts >= aff_7d && !str_eq(psnip, "") {
|
||||||
if str_eq(presult, "") { psnip } else { presult + "\n" + psnip }
|
if str_eq(pacc, "") { psnip } else { pacc + "\n" + psnip }
|
||||||
} else { presult }
|
} else { pacc }
|
||||||
let pi = pi + 1
|
let pi = pi + 1
|
||||||
presult
|
pacc
|
||||||
}
|
}
|
||||||
presult
|
pacc
|
||||||
} else { aff_ctx }
|
} else { aff_ctx }
|
||||||
if !str_eq(aff_ctx, "") {
|
if !str_eq(aff_ctx, "") {
|
||||||
state_set("soul_affective_context", aff_ctx)
|
state_set("soul_affective_context", aff_ctx)
|
||||||
println("[soul] cross-session affective context loaded (" + int_to_str(str_len(aff_ctx)) + " chars)")
|
println("[soul] affective context loaded (" + int_to_str(str_len(aff_ctx)) + " chars)")
|
||||||
}
|
|
||||||
|
|
||||||
// Issue 4/10 fix: scan BellEvent nodes for recent distress and cache in state.
|
|
||||||
// chat.el reads "soul_affective_context" at session start to avoid duplicating this
|
|
||||||
// search on every first message. Timestamp extracted from embedded " | ts:" marker
|
|
||||||
// first; falls back to created_at when absent (Issue 10 fix). Window: 14 days.
|
|
||||||
let aff_nodes: String = engram_search_json("BellEvent bell:soft bell:hard distress crisis upset hopeless", 5)
|
|
||||||
let aff_has: Bool = !str_eq(aff_nodes, "") && !str_eq(aff_nodes, "[]")
|
|
||||||
if aff_has {
|
|
||||||
let aff_now: Int = time_now()
|
|
||||||
let aff_cutoff: Int = aff_now - 1209600
|
|
||||||
let aff_node: String = json_array_get(aff_nodes, 0)
|
|
||||||
let aff_content: String = json_get(aff_node, "content")
|
|
||||||
let ts_marker: String = " | ts:"
|
|
||||||
let ts_pos: Int = str_index_of(aff_content, ts_marker)
|
|
||||||
let aff_ts_raw: String = if ts_pos >= 0 {
|
|
||||||
let ts_start: Int = ts_pos + str_len(ts_marker)
|
|
||||||
let rest: String = str_slice(aff_content, ts_start, str_len(aff_content))
|
|
||||||
let next_sep: Int = str_index_of(rest, " | ")
|
|
||||||
if next_sep < 0 { rest } else { str_slice(rest, 0, next_sep) }
|
|
||||||
} else {
|
|
||||||
let ca: String = json_get(aff_node, "created_at")
|
|
||||||
if str_eq(ca, "") { json_get(aff_node, "updated_at") } else { ca }
|
|
||||||
}
|
|
||||||
let aff_ts: Int = if str_eq(aff_ts_raw, "") { 0 } else { str_to_int(aff_ts_raw) }
|
|
||||||
if aff_ts > aff_cutoff {
|
|
||||||
state_set("soul_affective_context", "[RECENT CONTEXT: User recently expressed significant distress. Monitor for indirect crisis signals and respond with care.]")
|
|
||||||
println("[soul] affective context loaded — distress signal within 14d window")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,8 +365,11 @@ fn layered_cycle(raw_input: String) -> String {
|
|||||||
let cont_status: String = json_get(continuity, "status")
|
let cont_status: String = json_get(continuity, "status")
|
||||||
let cont_action: String = json_get(continuity, "action")
|
let cont_action: String = json_get(continuity, "action")
|
||||||
|
|
||||||
// Store continuity status so imprint can adjust its response register
|
// Store continuity status so imprint can adjust its response register.
|
||||||
state_set("session_continuity", cont_status)
|
// TODO(reliability #4): session_continuity is process-global; scope per session_id
|
||||||
|
// when available to prevent cross-session bleed under concurrent layered_cycle calls.
|
||||||
|
let cont_key: String = if str_eq(session_id, "") { "session_continuity" } else { "session_continuity:" + session_id }
|
||||||
|
state_set(cont_key, cont_status)
|
||||||
|
|
||||||
// Identity anomaly: add a gentle verification cue to the input before imprint
|
// Identity anomaly: add a gentle verification cue to the input before imprint
|
||||||
let guided: String = if str_eq(cont_action, "identity_check") {
|
let guided: String = if str_eq(cont_action, "identity_check") {
|
||||||
@@ -421,9 +392,7 @@ fn layered_cycle(raw_input: String) -> String {
|
|||||||
json_get(steward_result, "redirect_to")
|
json_get(steward_result, "redirect_to")
|
||||||
}
|
}
|
||||||
|
|
||||||
// L2c: affective context injection — augment safety addendum with recent emotional history.
|
// L2c: affective context injection.
|
||||||
// Ensures cross-session affective awareness is active even when soul_affective_context
|
|
||||||
// was not injected by build_system_prompt (belt-and-suspenders path).
|
|
||||||
let lc_aff_cutoff: Int = time_now() - 259200
|
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_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_has_bell: Bool = !str_eq(lc_bell_nodes, "") && !str_eq(lc_bell_nodes, "[]")
|
||||||
|
|||||||
Reference in New Issue
Block a user