feat(recall): cross-session-continuity improvements
Neuron Soul CI / build (pull_request) Failing after 14m49s

This commit is contained in:
2026-06-22 13:00:17 -05:00
parent 87c7d15b67
commit 795b32ad1a
3 changed files with 193 additions and 16 deletions
+118 -5
View File
@@ -429,6 +429,28 @@ fn handle_chat(body: String) -> String {
let profile_ok: Bool = !str_eq(profile_nodes, "") && !str_eq(profile_nodes, "[]")
let work_ok: Bool = !str_eq(work_nodes, "") && !str_eq(work_nodes, "[]")
// Load the previous session summary. Primary: label-based fetch (stable, written
// by session_summary_write). Fallback: vector search for SessionSummary nodes.
// Fixes issue #2: prev session summary was never loaded at startup.
let prev_sum_node: String = engram_get_node_by_label("session:summary")
let prev_sum_label_ok: Bool = !str_eq(prev_sum_node, "") && !str_eq(prev_sum_node, "null")
let prev_summary_raw: String = if prev_sum_label_ok {
json_get(prev_sum_node, "content")
} else {
let sum_nodes: String = engram_search_json("SessionSummary session:summary 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).
let profile_bullets: String = if profile_ok {
let pn: Int = json_array_len(profile_nodes)
@@ -476,15 +498,19 @@ fn handle_chat(body: String) -> String {
let has_profile: Bool = !str_eq(profile_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 {
"[USER CONTEXT from memory]\n" + profile_bullets
"[USER CONTEXT — from memory]\n" + profile_bullets
} else { "" }
let work_section: String = if has_work {
"[ACTIVE WORK from memory]\n" + work_bullets
"[ACTIVE WORK — from memory]\n" + work_bullets
} 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 { "" }
"\n\n" + profile_section + sep_pw + work_section
"\n\n" + summary_section + sep_sp + profile_section + sep_pw + work_section
} else { "" }
preload
} else { "" }
@@ -526,6 +552,14 @@ fn handle_chat(body: String) -> String {
state_set("conv_history", 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 act_ok: Bool = !str_eq(activation_nodes, "") && !str_eq(activation_nodes, "[]")
let act_out: String = if act_ok { activation_nodes } else { "[]" }
@@ -968,7 +1002,9 @@ fn handle_chat_agentic(body: String) -> String {
// 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.
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_action: String = json_get(screen_result, "action")
if str_eq(screen_action, "hard_bell") {
@@ -1041,6 +1077,24 @@ fn handle_chat_agentic(body: String) -> String {
let updated2: String = hist_append(updated, "assistant", reply_text)
let trimmed: String = if json_array_len(updated2) > 20 { hist_trim(updated2) } else { updated2 }
state_set(hist_key, trimmed)
// Persist to engram for cross-restart continuity.
// Named sessions get session-scoped labels, fixing ephemeral-only limitation (issue #4).
if str_eq(hist_key, "conv_history") {
conv_history_persist(trimmed)
} else {
if !str_eq(trimmed, "") && !str_eq(trimmed, "[]") {
let sess_hist_label: String = "conv:history:" + req_session
let sess_hist_tags: String = "[\"session-history\",\"persistent\"]"
let sess_hist_id: String = engram_node_full(
trimmed, "Conversation", sess_hist_label,
el_from_float(0.6), el_from_float(0.7), el_from_float(0.8),
"Episodic", sess_hist_tags
)
if str_eq(sess_hist_id, "") {
println("[chat] agentic: named session history persist failed for session=" + req_session)
}
}
}
true
} else { false }
@@ -1494,6 +1548,65 @@ fn handle_dharma_room_turn_agentic(body: String) -> String {
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.
let old_node: String = engram_get_node_by_label("session:summary")
let old_ok: Bool = !str_eq(old_node, "") && !str_eq(old_node, "null")
if old_ok {
let old_id: String = json_get(old_node, "id")
if !str_eq(old_id, "") {
engram_forget(old_id)
}
}
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")
if str_eq(role, "user") {
let msg: String = json_get(entry, "content")
let snip: String = if str_len(msg) > 80 { str_slice(msg, 0, 80) } else { msg }
let snippets = if str_eq(snippets, "") { snip } else { snippets + "; " + snip }
let count = count + 1
}
let i = i + 1
}
if str_eq(snippets, "") { return "" }
return "Session covered: " + snippets
}
fn auto_persist(req: String, resp: String) -> Void {
let message: String = json_get(req, "message")
let reply: String = json_get(resp, "response")