Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 95cb49a8b0 | |||
| 795b32ad1a |
@@ -181,31 +181,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 +241,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 {
|
||||||
@@ -371,11 +347,26 @@ fn clean_llm_response(s: String) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// conv_history_persist — save conversation history to engram for cross-restart continuity.
|
// conv_history_persist — save conversation history to engram for cross-restart continuity.
|
||||||
// Stores as a Conversation node. Overwrites by using consistent label "conv:history".
|
// Delete-before-write under label "conv:history" prevents unbounded node accumulation (issue #11).
|
||||||
fn conv_history_persist(hist: String) -> Void {
|
fn conv_history_persist(hist: String) -> Void {
|
||||||
if str_eq(hist, "") { return "" }
|
if str_eq(hist, "") { return "" }
|
||||||
if str_eq(hist, "[]") { return "" }
|
if str_eq(hist, "[]") { return "" }
|
||||||
let ts: Int = time_now()
|
// Delete any existing conv:history nodes before writing to avoid accumulation.
|
||||||
|
let old_hist_results: String = engram_search_json("conv:history", 3)
|
||||||
|
let old_hist_ok: Bool = !str_eq(old_hist_results, "") && !str_eq(old_hist_results, "[]")
|
||||||
|
if old_hist_ok {
|
||||||
|
let ohr_total: Int = json_array_len(old_hist_results)
|
||||||
|
let ohr_i: Int = 0
|
||||||
|
while ohr_i < ohr_total {
|
||||||
|
let ohr_node: String = json_array_get(old_hist_results, ohr_i)
|
||||||
|
let ohr_label: String = json_get(ohr_node, "label")
|
||||||
|
let ohr_id: String = json_get(ohr_node, "id")
|
||||||
|
if str_eq(ohr_label, "conv:history") && !str_eq(ohr_id, "") {
|
||||||
|
engram_forget(ohr_id)
|
||||||
|
}
|
||||||
|
let ohr_i = ohr_i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
let tags: String = "[\"conv-history\",\"persistent\"]"
|
let tags: String = "[\"conv-history\",\"persistent\"]"
|
||||||
let discard: String = engram_node_full(
|
let discard: String = engram_node_full(
|
||||||
hist, "Conversation", "conv:history",
|
hist, "Conversation", "conv:history",
|
||||||
@@ -424,62 +415,29 @@ 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 = {
|
// Fixes issue #6: soul_affective_context is pre-loaded at boot — use it first to
|
||||||
// Runs every turn. Uses correct BellEvent/PositiveEvent tags.
|
// avoid a redundant engram search and to make the boot-time state key functional.
|
||||||
let aff_now_ts: Int = time_now()
|
let affective_prefix: String = if hist_len == 0 {
|
||||||
let aff_cutoff: Int = aff_now_ts - 259200
|
let soul_aff_ctx: String = state_get("soul_affective_context")
|
||||||
let boot_aff: String = state_get("soul_affective_context")
|
let found_recent: Bool = if !str_eq(soul_aff_ctx, "") {
|
||||||
let has_boot_aff: Bool = !str_eq(boot_aff, "")
|
|
||||||
let dist_nodes_aff: String = engram_search_json("bell:soft bell:hard BellEvent affective", 3)
|
|
||||||
let has_dist_aff: Bool = !str_eq(dist_nodes_aff, "") && !str_eq(dist_nodes_aff, "[]")
|
|
||||||
let found_recent_dist: Bool = if has_boot_aff {
|
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
if has_dist_aff {
|
let distress_nodes: String = engram_search_json("bell distress crisis loss grief despair", 3)
|
||||||
let dn0: String = json_array_get(dist_nodes_aff, 0)
|
let has_nodes: Bool = !str_eq(distress_nodes, "") && !str_eq(distress_nodes, "[]")
|
||||||
let dn_content: String = json_get(dn0, "content")
|
let now_ts: Int = time_now()
|
||||||
let daff_marker: String = " | ts:"
|
let cutoff: Int = now_ts - 259200
|
||||||
let daff_pos: Int = str_index_of(dn_content, daff_marker)
|
if has_nodes {
|
||||||
let daff_ts_str: String = if daff_pos >= 0 {
|
let dn0: String = json_array_get(distress_nodes, 0)
|
||||||
let daff_start: Int = daff_pos + str_len(daff_marker)
|
let ts0_raw: String = json_get(dn0, "created_at")
|
||||||
let daff_rest: String = str_slice(dn_content, daff_start, str_len(dn_content))
|
let ts0_str: String = if str_eq(ts0_raw, "") { json_get(dn0, "updated_at") } else { ts0_raw }
|
||||||
let daff_next: Int = str_index_of(daff_rest, " | ")
|
let ts0: Int = if str_eq(ts0_str, "") { 0 } else { str_to_int(ts0_str) }
|
||||||
if daff_next < 0 { daff_rest } else { str_slice(daff_rest, 0, daff_next) }
|
ts0 > cutoff
|
||||||
} 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 }
|
} else { false }
|
||||||
}
|
}
|
||||||
let pos_nodes_aff: String = engram_search_json("PositiveEvent joy:high joy:low affective", 3)
|
if found_recent {
|
||||||
let has_pos_aff: Bool = !str_eq(pos_nodes_aff, "") && !str_eq(pos_nodes_aff, "[]")
|
|
||||||
let found_recent_pos: Bool = if has_pos_aff && !found_recent_dist {
|
|
||||||
let pn0: String = json_array_get(pos_nodes_aff, 0)
|
|
||||||
let pn_content: String = json_get(pn0, "content")
|
|
||||||
let paff_marker: String = " | ts:"
|
|
||||||
let paff_pos: Int = str_index_of(pn_content, paff_marker)
|
|
||||||
let paff_ts_str: String = if paff_pos >= 0 {
|
|
||||||
let paff_start: Int = paff_pos + str_len(paff_marker)
|
|
||||||
let paff_rest: String = str_slice(pn_content, paff_start, str_len(pn_content))
|
|
||||||
let paff_next: Int = str_index_of(paff_rest, " | ")
|
|
||||||
if paff_next < 0 { paff_rest } else { str_slice(paff_rest, 0, paff_next) }
|
|
||||||
} else {
|
|
||||||
let paff_ca: String = json_get(pn0, "created_at")
|
|
||||||
if str_eq(paff_ca, "") { json_get(pn0, "updated_at") } else { paff_ca }
|
|
||||||
}
|
|
||||||
let paff_ts: Int = if str_eq(paff_ts_str, "") { 0 } else { str_to_int(paff_ts_str) }
|
|
||||||
paff_ts > aff_cutoff
|
|
||||||
} else { false }
|
|
||||||
if found_recent_dist {
|
|
||||||
"[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)
|
||||||
@@ -493,6 +451,49 @@ fn handle_chat(body: String) -> String {
|
|||||||
let profile_ok: Bool = !str_eq(profile_nodes, "") && !str_eq(profile_nodes, "[]")
|
let profile_ok: Bool = !str_eq(profile_nodes, "") && !str_eq(profile_nodes, "[]")
|
||||||
let work_ok: Bool = !str_eq(work_nodes, "") && !str_eq(work_nodes, "[]")
|
let work_ok: Bool = !str_eq(work_nodes, "") && !str_eq(work_nodes, "[]")
|
||||||
|
|
||||||
|
// Load the previous session summary. Search by label text + type, then filter by
|
||||||
|
// exact label match. Fallback: broader vector search for SessionSummary nodes.
|
||||||
|
// Fixes issue #2: prev session summary was never loaded at startup.
|
||||||
|
// Fixes issue #2b (phantom engram_get_node_by_label replaced with engram_search_json).
|
||||||
|
let sum_search_nodes: String = engram_search_json("session:summary SessionSummary", 5)
|
||||||
|
let sum_search_ok: Bool = !str_eq(sum_search_nodes, "") && !str_eq(sum_search_nodes, "[]")
|
||||||
|
let prev_sum_node_content: String = if sum_search_ok {
|
||||||
|
let ss_total: Int = json_array_len(sum_search_nodes)
|
||||||
|
let ssi: Int = 0
|
||||||
|
let found_content: String = ""
|
||||||
|
while ssi < ss_total {
|
||||||
|
let ss_node: String = json_array_get(sum_search_nodes, ssi)
|
||||||
|
let ss_label: String = json_get(ss_node, "label")
|
||||||
|
let ss_type: String = json_get(ss_node, "node_type")
|
||||||
|
let ss_content: String = json_get(ss_node, "content")
|
||||||
|
let found_content = if str_eq(ss_label, "session:summary") && str_eq(ss_type, "SessionSummary") && !str_eq(ss_content, "") {
|
||||||
|
if str_eq(found_content, "") { ss_content } else { found_content }
|
||||||
|
} else { found_content }
|
||||||
|
let ssi = ssi + 1
|
||||||
|
}
|
||||||
|
found_content
|
||||||
|
} else { "" }
|
||||||
|
// Check state first: soul.el pre-loads this at boot (soul_prev_session_summary) — fixes issue #5.
|
||||||
|
let soul_cached_sum: String = state_get("soul_prev_session_summary")
|
||||||
|
let prev_summary_raw: String = if !str_eq(soul_cached_sum, "") {
|
||||||
|
soul_cached_sum
|
||||||
|
} else if !str_eq(prev_sum_node_content, "") {
|
||||||
|
prev_sum_node_content
|
||||||
|
} else {
|
||||||
|
let sum_nodes: String = engram_search_json("SessionSummary previous-session", 3)
|
||||||
|
let sum_ok: Bool = !str_eq(sum_nodes, "") && !str_eq(sum_nodes, "[]")
|
||||||
|
if sum_ok {
|
||||||
|
let sn0: String = json_array_get(sum_nodes, 0)
|
||||||
|
let stype: String = json_get(sn0, "node_type")
|
||||||
|
let scontent: String = json_get(sn0, "content")
|
||||||
|
if str_eq(stype, "SessionSummary") && !str_eq(scontent, "") { scontent } else { "" }
|
||||||
|
} else { "" }
|
||||||
|
}
|
||||||
|
let has_prev_summary: Bool = !str_eq(prev_summary_raw, "")
|
||||||
|
let prev_summary_snip: String = if str_len(prev_summary_raw) > 400 {
|
||||||
|
str_slice(prev_summary_raw, 0, 400)
|
||||||
|
} else { prev_summary_raw }
|
||||||
|
|
||||||
// Extract content fields and render as bullet points (one per node, first 120 chars).
|
// Extract content fields and render as bullet points (one per node, first 120 chars).
|
||||||
let profile_bullets: String = if profile_ok {
|
let profile_bullets: String = if profile_ok {
|
||||||
let pn: Int = json_array_len(profile_nodes)
|
let pn: Int = json_array_len(profile_nodes)
|
||||||
@@ -540,15 +541,19 @@ fn handle_chat(body: String) -> String {
|
|||||||
|
|
||||||
let has_profile: Bool = !str_eq(profile_bullets, "")
|
let has_profile: Bool = !str_eq(profile_bullets, "")
|
||||||
let has_work: Bool = !str_eq(work_bullets, "")
|
let has_work: Bool = !str_eq(work_bullets, "")
|
||||||
let preload: String = if has_profile || has_work {
|
let preload: String = if has_profile || has_work || has_prev_summary {
|
||||||
|
let summary_section: String = if has_prev_summary {
|
||||||
|
"[PREVIOUS SESSION - what we discussed last time]\n" + prev_summary_snip
|
||||||
|
} else { "" }
|
||||||
let profile_section: String = if has_profile {
|
let profile_section: String = if has_profile {
|
||||||
"[USER CONTEXT — from memory]\n" + profile_bullets
|
"[USER CONTEXT - from memory]\n" + profile_bullets
|
||||||
} else { "" }
|
} else { "" }
|
||||||
let work_section: String = if has_work {
|
let work_section: String = if has_work {
|
||||||
"[ACTIVE WORK — from memory]\n" + work_bullets
|
"[ACTIVE WORK - from memory]\n" + work_bullets
|
||||||
} else { "" }
|
} else { "" }
|
||||||
|
let sep_sp: String = if has_prev_summary && (has_profile || has_work) { "\n\n" } else { "" }
|
||||||
let sep_pw: String = if has_profile && has_work { "\n\n" } else { "" }
|
let sep_pw: String = if has_profile && has_work { "\n\n" } else { "" }
|
||||||
"\n\n" + profile_section + sep_pw + work_section
|
"\n\n" + summary_section + sep_sp + profile_section + sep_pw + work_section
|
||||||
} else { "" }
|
} else { "" }
|
||||||
preload
|
preload
|
||||||
} else { "" }
|
} else { "" }
|
||||||
@@ -590,6 +595,14 @@ fn handle_chat(body: String) -> String {
|
|||||||
state_set("conv_history", final_hist)
|
state_set("conv_history", final_hist)
|
||||||
conv_history_persist(final_hist)
|
conv_history_persist(final_hist)
|
||||||
|
|
||||||
|
// Automatic session-end summary: write/overwrite the SessionSummary node on each turn
|
||||||
|
// so process restarts always have a continuity snapshot (no shutdown hook needed).
|
||||||
|
// Uses autogenerate (no LLM) so it is cheap — the node is overwritten not appended.
|
||||||
|
let auto_sum: String = session_summary_autogenerate(final_hist)
|
||||||
|
if !str_eq(auto_sum, "") {
|
||||||
|
let discard_sum: String = session_summary_write(auto_sum)
|
||||||
|
}
|
||||||
|
|
||||||
let activation_nodes: String = engram_activate_json(message, 2)
|
let activation_nodes: String = engram_activate_json(message, 2)
|
||||||
let act_ok: Bool = !str_eq(activation_nodes, "") && !str_eq(activation_nodes, "[]")
|
let act_ok: Bool = !str_eq(activation_nodes, "") && !str_eq(activation_nodes, "[]")
|
||||||
let act_out: String = if act_ok { activation_nodes } else { "[]" }
|
let act_out: String = if act_ok { activation_nodes } else { "[]" }
|
||||||
@@ -1032,13 +1045,15 @@ fn handle_chat_agentic(body: String) -> String {
|
|||||||
|
|
||||||
// L1 safety screen — agentic path must pass the same gate as layered_cycle.
|
// L1 safety screen — agentic path must pass the same gate as layered_cycle.
|
||||||
// Hard bell: return the crisis response immediately, do not enter the agentic loop.
|
// Hard bell: return the crisis response immediately, do not enter the agentic loop.
|
||||||
let history: String = state_get("conversation_history")
|
// Fix(issue #9): "conversation_history" key was never written; history lives under "conv_history".
|
||||||
|
// Old key caused history-amplification in safety_screen to always receive "" on agentic path.
|
||||||
|
let history: String = state_get("conv_history")
|
||||||
let screen_result: String = safety_screen(message, history)
|
let screen_result: String = safety_screen(message, history)
|
||||||
let screen_action: String = json_get(screen_result, "action")
|
let screen_action: String = json_get(screen_result, "action")
|
||||||
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 }
|
||||||
@@ -1062,7 +1077,14 @@ fn handle_chat_agentic(body: String) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let hist_key: String = if str_eq(req_session, "") { "conv_history" } else { "session_hist_" + req_session }
|
let hist_key: String = if str_eq(req_session, "") { "conv_history" } else { "session_hist_" + req_session }
|
||||||
let agentic_hist: String = state_get(hist_key)
|
// Fall back to engram (via session_hist_load) when state is cold — fixes issue #4:
|
||||||
|
// named-session history written under session:messages:SESSION_ID was never read back.
|
||||||
|
let agentic_hist_state: String = state_get(hist_key)
|
||||||
|
let agentic_hist: String = if str_eq(agentic_hist_state, "") && !str_eq(req_session, "") {
|
||||||
|
let loaded: String = session_hist_load(req_session)
|
||||||
|
if !str_eq(loaded, "") { state_set(hist_key, loaded) }
|
||||||
|
if str_eq(loaded, "") { conv_history_load() } else { loaded }
|
||||||
|
} else { agentic_hist_state }
|
||||||
let agentic_hist_len: Int = if str_eq(agentic_hist, "") { 0 } else { json_array_len(agentic_hist) }
|
let agentic_hist_len: Int = if str_eq(agentic_hist, "") { 0 } else { json_array_len(agentic_hist) }
|
||||||
let ag_is_cont: Bool = str_len(message) < 50 && agentic_hist_len > 0
|
let ag_is_cont: Bool = str_len(message) < 50 && agentic_hist_len > 0
|
||||||
let ag_last_entry: String = if ag_is_cont { json_array_get(agentic_hist, agentic_hist_len - 1) } else { "" }
|
let ag_last_entry: String = if ag_is_cont { json_array_get(agentic_hist, agentic_hist_len - 1) } else { "" }
|
||||||
@@ -1105,6 +1127,23 @@ fn handle_chat_agentic(body: String) -> String {
|
|||||||
let updated2: String = hist_append(updated, "assistant", reply_text)
|
let updated2: String = hist_append(updated, "assistant", reply_text)
|
||||||
let trimmed: String = if json_array_len(updated2) > 20 { hist_trim(updated2) } else { updated2 }
|
let trimmed: String = if json_array_len(updated2) > 20 { hist_trim(updated2) } else { updated2 }
|
||||||
state_set(hist_key, trimmed)
|
state_set(hist_key, trimmed)
|
||||||
|
// Persist to engram for cross-restart continuity.
|
||||||
|
// Named sessions use session_hist_save (session:messages:SESSION_ID label) so that
|
||||||
|
// session_hist_load can recover them on the next restart — fixes issue #4.
|
||||||
|
// The old conv:history:SESSION_ID label was a dead write (never read back).
|
||||||
|
if str_eq(hist_key, "conv_history") {
|
||||||
|
conv_history_persist(trimmed)
|
||||||
|
} else {
|
||||||
|
if !str_eq(trimmed, "") && !str_eq(trimmed, "[]") {
|
||||||
|
session_hist_save(req_session, trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Write automatic session summary so cross-session continuity is maintained
|
||||||
|
// on the agentic path too — fixes issue #7.
|
||||||
|
let ag_auto_sum: String = session_summary_autogenerate(trimmed)
|
||||||
|
if !str_eq(ag_auto_sum, "") {
|
||||||
|
let discard_ag_sum: String = session_summary_write(ag_auto_sum)
|
||||||
|
}
|
||||||
true
|
true
|
||||||
} else { false }
|
} else { false }
|
||||||
|
|
||||||
@@ -1558,6 +1597,74 @@ fn handle_dharma_room_turn_agentic(body: String) -> String {
|
|||||||
return "{\"response\":\"" + safe_text + "\",\"cgi_id\":\"" + cgi_id + "\",\"tools_used\":" + eff_tools + "}"
|
return "{\"response\":\"" + safe_text + "\",\"cgi_id\":\"" + cgi_id + "\",\"tools_used\":" + eff_tools + "}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// session_summary_write — write or overwrite the SessionSummary node in engram.
|
||||||
|
// Uses delete-before-write so there is always exactly one "session:summary" node.
|
||||||
|
// This is what session_preload at next startup reads to know what was discussed.
|
||||||
|
fn session_summary_write(summary_text: String) -> String {
|
||||||
|
if str_eq(summary_text, "") { return "" }
|
||||||
|
let safe_text: String = str_replace(summary_text, "\"", "'")
|
||||||
|
let trimmed: String = if str_len(safe_text) > 800 { str_slice(safe_text, 0, 800) } else { safe_text }
|
||||||
|
let ts: Int = time_now()
|
||||||
|
let ts_str: String = int_to_str(ts)
|
||||||
|
let content: String = "[session-summary] " + trimmed + " | ts:" + ts_str
|
||||||
|
// Delete old node before writing so duplicate label nodes don't accumulate.
|
||||||
|
// engram_get_node_by_label doesn't exist — search by label text and filter by exact match.
|
||||||
|
let old_search: String = engram_search_json("session:summary SessionSummary", 5)
|
||||||
|
let old_search_ok: Bool = !str_eq(old_search, "") && !str_eq(old_search, "[]")
|
||||||
|
if old_search_ok {
|
||||||
|
let os_total: Int = json_array_len(old_search)
|
||||||
|
let osi: Int = 0
|
||||||
|
while osi < os_total {
|
||||||
|
let os_node: String = json_array_get(old_search, osi)
|
||||||
|
let os_label: String = json_get(os_node, "label")
|
||||||
|
let os_id: String = json_get(os_node, "id")
|
||||||
|
if str_eq(os_label, "session:summary") && !str_eq(os_id, "") {
|
||||||
|
engram_forget(os_id)
|
||||||
|
}
|
||||||
|
let osi = osi + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let tags: String = "[\"SessionSummary\",\"session-summary\",\"previous-session\",\"consolidate\"]"
|
||||||
|
let node_id: String = engram_node_full(
|
||||||
|
content, "SessionSummary", "session:summary",
|
||||||
|
el_from_float(0.85), el_from_float(0.85), el_from_float(1.0),
|
||||||
|
"Episodic", tags
|
||||||
|
)
|
||||||
|
if str_eq(node_id, "") {
|
||||||
|
println("[chat] session_summary_write: engram write failed — summary node lost")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
println("[chat] session_summary_write: wrote SessionSummary (" + int_to_str(str_len(content)) + " chars) -> " + node_id)
|
||||||
|
return node_id
|
||||||
|
}
|
||||||
|
|
||||||
|
// session_summary_autogenerate — build a minimal summary from conversation history without LLM.
|
||||||
|
// Extracts user message snippets (first 80 chars each, up to 5 turns).
|
||||||
|
// Used as the automatic session-end hook so every turn produces a continuity snapshot.
|
||||||
|
fn session_summary_autogenerate(hist: String) -> String {
|
||||||
|
if str_eq(hist, "") { return "" }
|
||||||
|
if str_eq(hist, "[]") { return "" }
|
||||||
|
let total: Int = json_array_len(hist)
|
||||||
|
if total == 0 { return "" }
|
||||||
|
let snippets: String = ""
|
||||||
|
let count: Int = 0
|
||||||
|
let i: Int = 0
|
||||||
|
while i < total && count < 5 {
|
||||||
|
let entry: String = json_array_get(hist, i)
|
||||||
|
let role: String = json_get(entry, "role")
|
||||||
|
let msg: String = json_get(entry, "content")
|
||||||
|
let snip: String = if str_len(msg) > 80 { str_slice(msg, 0, 80) } else { msg }
|
||||||
|
// Mutations at top level of while body via if-expressions — inner if blocks don't escape scope.
|
||||||
|
let snippets = if str_eq(role, "user") && !str_eq(snip, "") {
|
||||||
|
if str_eq(snippets, "") { snip } else { snippets + "; " + snip }
|
||||||
|
} else { snippets }
|
||||||
|
let count = if str_eq(role, "user") && !str_eq(snip, "") { count + 1 } else { count }
|
||||||
|
let i = i + 1
|
||||||
|
}
|
||||||
|
if str_eq(snippets, "") { return "" }
|
||||||
|
return "Session covered: " + snippets
|
||||||
|
}
|
||||||
|
|
||||||
fn auto_persist(req: String, resp: String) -> Void {
|
fn auto_persist(req: String, resp: String) -> Void {
|
||||||
let message: String = json_get(req, "message")
|
let message: String = json_get(req, "message")
|
||||||
let reply: String = json_get(resp, "response")
|
let reply: String = json_get(resp, "response")
|
||||||
@@ -1574,18 +1681,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 +1773,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.
|
||||||
|
|||||||
+8
-8
@@ -1,4 +1,5 @@
|
|||||||
import "memory.el"
|
import "memory.el"
|
||||||
|
import "chat.el"
|
||||||
|
|
||||||
// neuron-api.el — Native Neuron cognitive API handlers.
|
// neuron-api.el — Native Neuron cognitive API handlers.
|
||||||
//
|
//
|
||||||
@@ -654,14 +655,13 @@ fn handle_api_consolidate(body: String) -> String {
|
|||||||
engram_save(snap)
|
engram_save(snap)
|
||||||
}
|
}
|
||||||
if !str_eq(summary, "") {
|
if !str_eq(summary, "") {
|
||||||
let safe_summary: String = str_replace(summary, "\"", "'")
|
// Use session_summary_write to ensure delete-before-write semantics:
|
||||||
let tags: String = "[\"SessionSummary\",\"consolidate\"]"
|
// prevents stale SessionSummary accumulation across sessions (issue #11).
|
||||||
let discard: String = engram_node_full(
|
// session_summary_write handles label indexing, trimming, and dedup.
|
||||||
"[session-summary] " + safe_summary,
|
let sum_id: String = session_summary_write(summary)
|
||||||
"SessionSummary", "session:summary",
|
if str_eq(sum_id, "") {
|
||||||
el_from_float(0.7), el_from_float(0.7), el_from_float(0.9),
|
println("[api] consolidate: session_summary_write failed — summary not persisted")
|
||||||
"Episodic", tags
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return "{\"ok\":true,\"snapshot\":\"" + snap + "\"}"
|
return "{\"ok\":true,\"snapshot\":\"" + snap + "\"}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -163,73 +163,46 @@ fn load_identity_context() -> Void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cross-session affective context: load BellEvent and PositiveEvent nodes from last 7 days.
|
// Cross-session affective context: query engram for recent distress/crisis signals.
|
||||||
let aff_now: Int = time_now()
|
// Broadened query includes session:emotional-summary and BellEvent tags (issue #10):
|
||||||
let aff_7d: Int = aff_now - 604800
|
// the old keywords-only search missed these nodes when their content lacked exact phrases.
|
||||||
let bell_raw: String = engram_search_json("bell:soft bell:hard BellEvent affective", 3)
|
// 7-day recency window applied via the "ts" field embedded in BellEvent content.
|
||||||
let bell_aff_ok: Bool = !str_eq(bell_raw, "") && !str_eq(bell_raw, "[]")
|
let affective_raw: String = engram_search_json("distress crisis upset hopeless session:emotional-summary BellEvent bell:hard bell:soft", 5)
|
||||||
let aff_ctx: String = ""
|
let affective_ok: Bool = !str_eq(affective_raw, "") && !str_eq(affective_raw, "[]")
|
||||||
let aff_ctx = if bell_aff_ok {
|
if affective_ok {
|
||||||
let bn_total: Int = json_array_len(bell_raw)
|
let ts_now: Int = time_now()
|
||||||
let bacc: String = ""
|
let ts_cutoff: Int = ts_now - 604800
|
||||||
let bi: Int = 0
|
let aff_total: Int = json_array_len(affective_raw)
|
||||||
let bacc = while bi < bn_total {
|
let aff_ctx: String = ""
|
||||||
let bn: String = json_array_get(bell_raw, bi)
|
let ai: Int = 0
|
||||||
let bn_c: String = json_get(bn, "content")
|
while ai < aff_total {
|
||||||
let bm: String = " | ts:"
|
let aff_node: String = json_array_get(affective_raw, ai)
|
||||||
let bmp: Int = str_index_of(bn_c, bm)
|
let aff_content: String = json_get(aff_node, "content")
|
||||||
let bn_ts_raw: String = if bmp >= 0 {
|
// Try multiple timestamp fields: "ts" (embedded), "created_at", "updated_at"
|
||||||
let bs: Int = bmp + str_len(bm)
|
let aff_ts_str: String = json_get(aff_node, "ts")
|
||||||
let br: String = str_slice(bn_c, bs, str_len(bn_c))
|
let aff_ts_str2: String = if str_eq(aff_ts_str, "") { json_get(aff_node, "created_at") } else { aff_ts_str }
|
||||||
let bn_next: Int = str_index_of(br, " | ")
|
// Also try embedded " | ts:NNN" format used in BellEvent content
|
||||||
if bn_next < 0 { br } else { str_slice(br, 0, bn_next) }
|
let ts_marker: String = " | ts:"
|
||||||
} else {
|
let ts_pos: Int = str_index_of(aff_content, ts_marker)
|
||||||
let bca: String = json_get(bn, "created_at")
|
let aff_ts_embedded: String = if ts_pos >= 0 {
|
||||||
if str_eq(bca, "") { json_get(bn, "updated_at") } else { bca }
|
let ts_start: Int = ts_pos + str_len(ts_marker)
|
||||||
}
|
let rest: String = str_slice(aff_content, ts_start, str_len(aff_content))
|
||||||
let bn_ts: Int = if str_eq(bn_ts_raw, "") { 0 } else { str_to_int(bn_ts_raw) }
|
let next_sep: Int = str_index_of(rest, " | ")
|
||||||
let snip: String = if str_len(bn_c) > 200 { str_slice(bn_c, 0, 200) } else { bn_c }
|
if next_sep < 0 { rest } else { str_slice(rest, 0, next_sep) }
|
||||||
let bacc = if bn_ts >= aff_7d && !str_eq(snip, "") {
|
} else { "" }
|
||||||
if str_eq(bacc, "") { snip } else { bacc + "\n" + snip }
|
let eff_ts_str: String = if !str_eq(aff_ts_embedded, "") { aff_ts_embedded } else { aff_ts_str2 }
|
||||||
} else { bacc }
|
let aff_ts: Int = if str_eq(eff_ts_str, "") { ts_now } else { str_to_int(eff_ts_str) }
|
||||||
let bi = bi + 1
|
let is_recent: Bool = aff_ts >= ts_cutoff
|
||||||
bacc
|
let snip: String = if str_len(aff_content) > 200 { str_slice(aff_content, 0, 200) } else { aff_content }
|
||||||
|
let aff_ctx = if is_recent && !str_eq(snip, "") {
|
||||||
|
if str_eq(aff_ctx, "") { snip } else { aff_ctx + "\n" + snip }
|
||||||
|
} else { aff_ctx }
|
||||||
|
let ai = ai + 1
|
||||||
}
|
}
|
||||||
bacc
|
if !str_eq(aff_ctx, "") {
|
||||||
} else { "" }
|
state_set("soul_affective_context", aff_ctx)
|
||||||
let pos_raw: String = engram_search_json("PositiveEvent joy:high joy:low affective", 3)
|
println("[soul] cross-session affective context loaded (" + int_to_str(str_len(aff_ctx)) + " chars)")
|
||||||
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)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,12 +275,59 @@ fn emit_session_start_event() -> Void {
|
|||||||
}
|
}
|
||||||
let ts: Int = time_now()
|
let ts: Int = time_now()
|
||||||
|
|
||||||
|
// Load previous session summary at boot — stash in state for session_preload.
|
||||||
|
// Search by label text + type, filter by exact label match to avoid false positives.
|
||||||
|
// engram_get_node_by_label is not a runtime builtin; engram_search_json is used instead.
|
||||||
|
let sum_boot_search: String = engram_search_json("session:summary SessionSummary", 5)
|
||||||
|
let sum_boot_ok: Bool = !str_eq(sum_boot_search, "") && !str_eq(sum_boot_search, "[]")
|
||||||
|
let prev_sum_content: String = if sum_boot_ok {
|
||||||
|
let sbs_total: Int = json_array_len(sum_boot_search)
|
||||||
|
let sbs_i: Int = 0
|
||||||
|
let sbs_found: String = ""
|
||||||
|
while sbs_i < sbs_total {
|
||||||
|
let sbs_node: String = json_array_get(sum_boot_search, sbs_i)
|
||||||
|
let sbs_label: String = json_get(sbs_node, "label")
|
||||||
|
let sbs_type: String = json_get(sbs_node, "node_type")
|
||||||
|
let sbs_content: String = json_get(sbs_node, "content")
|
||||||
|
let sbs_found = if str_eq(sbs_label, "session:summary") && str_eq(sbs_type, "SessionSummary") && !str_eq(sbs_content, "") {
|
||||||
|
if str_eq(sbs_found, "") { sbs_content } else { sbs_found }
|
||||||
|
} else { sbs_found }
|
||||||
|
let sbs_i = sbs_i + 1
|
||||||
|
}
|
||||||
|
if str_eq(sbs_found, "") {
|
||||||
|
let sum_fb: String = engram_search_json("SessionSummary previous-session", 2)
|
||||||
|
let sum_fb_ok: Bool = !str_eq(sum_fb, "") && !str_eq(sum_fb, "[]")
|
||||||
|
if sum_fb_ok {
|
||||||
|
let sfn: String = json_array_get(sum_fb, 0)
|
||||||
|
let sftype: String = json_get(sfn, "node_type")
|
||||||
|
let sfcontent: String = json_get(sfn, "content")
|
||||||
|
if str_eq(sftype, "SessionSummary") && !str_eq(sfcontent, "") { sfcontent } else { "" }
|
||||||
|
} else { "" }
|
||||||
|
} else { sbs_found }
|
||||||
|
} else {
|
||||||
|
let sum_fb2: String = engram_search_json("SessionSummary previous-session", 2)
|
||||||
|
let sum_fb2_ok: Bool = !str_eq(sum_fb2, "") && !str_eq(sum_fb2, "[]")
|
||||||
|
if sum_fb2_ok {
|
||||||
|
let sfn2: String = json_array_get(sum_fb2, 0)
|
||||||
|
let sftype2: String = json_get(sfn2, "node_type")
|
||||||
|
let sfcontent2: String = json_get(sfn2, "content")
|
||||||
|
if str_eq(sftype2, "SessionSummary") && !str_eq(sfcontent2, "") { sfcontent2 } else { "" }
|
||||||
|
} else { "" }
|
||||||
|
}
|
||||||
|
let has_prev_sum: String = if str_eq(prev_sum_content, "") { "false" } else { "true" }
|
||||||
|
if !str_eq(prev_sum_content, "") {
|
||||||
|
state_set("soul_prev_session_summary", prev_sum_content)
|
||||||
|
println("[soul] previous session summary loaded (" + int_to_str(str_len(prev_sum_content)) + " chars)")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
let payload: String = "{\"event\":\"session_start\""
|
let payload: String = "{\"event\":\"session_start\""
|
||||||
+ ",\"boot\":" + boot_num
|
+ ",\"boot\":" + boot_num
|
||||||
+ ",\"cgi\":\"" + eff_cgi + "\""
|
+ ",\"cgi\":\"" + eff_cgi + "\""
|
||||||
+ ",\"node_count\":" + int_to_str(node_ct)
|
+ ",\"node_count\":" + int_to_str(node_ct)
|
||||||
+ ",\"edge_count\":" + int_to_str(edge_ct)
|
+ ",\"edge_count\":" + int_to_str(edge_ct)
|
||||||
+ ",\"identity_loaded\":" + has_identity
|
+ ",\"identity_loaded\":" + has_identity
|
||||||
|
+ ",\"prev_session_summary_loaded\":" + has_prev_sum
|
||||||
+ ",\"ts\":" + int_to_str(ts) + "}"
|
+ ",\"ts\":" + int_to_str(ts) + "}"
|
||||||
|
|
||||||
let tags: String = "[\"internal-state\",\"session-start\",\"InternalStateEvent\"]"
|
let tags: String = "[\"internal-state\",\"session-start\",\"InternalStateEvent\"]"
|
||||||
@@ -316,7 +336,7 @@ fn emit_session_start_event() -> Void {
|
|||||||
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),
|
||||||
"Episodic", tags
|
"Episodic", tags
|
||||||
)
|
)
|
||||||
println("[soul] session-start event logged (boot=" + boot_num + " nodes=" + int_to_str(node_ct) + " edges=" + int_to_str(edge_ct) + ")")
|
println("[soul] session-start event logged (boot=" + boot_num + " nodes=" + int_to_str(node_ct) + " edges=" + int_to_str(edge_ct) + " prev_summary=" + has_prev_sum + ")")
|
||||||
}
|
}
|
||||||
|
|
||||||
// layered_cycle — routes user-facing requests through the 4-layer consciousness stack.
|
// layered_cycle — routes user-facing requests through the 4-layer consciousness stack.
|
||||||
@@ -389,53 +409,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