fix(recall): session-end summary hook + session summary recall at start
This commit is contained in:
@@ -265,6 +265,39 @@ fn engram_nodes_merge(a: String, b: String) -> String {
|
||||
return engram_dedup_nodes("[" + ai + "," + bi + "]")
|
||||
}
|
||||
|
||||
// id_in_seen — check if node_id appears in the comma-delimited seen accumulator.
|
||||
// Pads both sides with commas to avoid false substring matches.
|
||||
fn id_in_seen(node_id: String, seen: String) -> Bool {
|
||||
if str_eq(node_id, "") { return false }
|
||||
if str_eq(seen, "") { return false }
|
||||
return str_contains("," + seen + ",", "," + node_id + ",")
|
||||
}
|
||||
|
||||
// add_to_seen — append node_id to the comma-delimited seen accumulator.
|
||||
fn add_to_seen(seen: String, node_id: String) -> String {
|
||||
if str_eq(node_id, "") { return seen }
|
||||
if str_eq(seen, "") { return node_id }
|
||||
return seen + "," + node_id
|
||||
}
|
||||
|
||||
// engram_extract_ids — extract all non-empty "id" fields from a JSON node array
|
||||
// into a comma-delimited string for use with id_in_seen / add_to_seen.
|
||||
fn engram_extract_ids(nodes_json: String) -> String {
|
||||
if str_eq(nodes_json, "") { return "" }
|
||||
if str_eq(nodes_json, "[]") { return "" }
|
||||
let total: Int = json_array_len(nodes_json)
|
||||
if total == 0 { return "" }
|
||||
let ids: String = ""
|
||||
let i: Int = 0
|
||||
while i < total {
|
||||
let node: String = json_array_get(nodes_json, i)
|
||||
let nid: String = json_get(node, "id")
|
||||
let ids = if str_eq(nid, "") { ids } else { add_to_seen(ids, nid) }
|
||||
let i = i + 1
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
fn engram_compile(intent: String) -> String {
|
||||
// Issue 1: decompose multi-topic messages into sub-queries.
|
||||
let topics: String = engram_split_topics(intent)
|
||||
@@ -347,6 +380,11 @@ fn engram_compile(intent: String) -> String {
|
||||
let merged: String = engram_nodes_merge(merged, recall_boost)
|
||||
let merged_nodes: String = merged
|
||||
|
||||
// Dedup fix: publish seen node IDs so downstream callers (session_preload, affective_prefix)
|
||||
// can skip nodes already present here. EL has no tuple returns so we use state as out-param.
|
||||
let compile_seen_ids: String = engram_extract_ids(merged_nodes)
|
||||
state_set("engram_compile_seen_ids", compile_seen_ids)
|
||||
|
||||
// Fallback: when all searches return nothing, fetch persona nodes.
|
||||
let scan_part: String = if str_eq(merged_nodes, "") || str_eq(merged_nodes, "[]") {
|
||||
let persona_fallback: String = engram_search_json("soul:persona Persona identity", 5)
|
||||
@@ -703,22 +741,41 @@ fn handle_chat(body: String) -> String {
|
||||
pb
|
||||
} else { "" }
|
||||
|
||||
let summary_bullet: String = if summary_ok {
|
||||
let sn0: String = json_array_get(summary_nodes, 0)
|
||||
let sc0: String = json_get(sn0, "content")
|
||||
let ss0: String = if str_len(sc0) > 200 { str_slice(sc0, 0, 200) } else { sc0 }
|
||||
if str_eq(ss0, "") { "" } else { "- " + ss0 }
|
||||
// Session summary recall: show up to 3 previous session summaries so the soul
|
||||
// knows what was discussed in recent past conversations.
|
||||
let summary_bullets: String = if summary_ok {
|
||||
let sn_total: Int = json_array_len(summary_nodes)
|
||||
let sb: String = ""
|
||||
let sb = if sn_total > 0 {
|
||||
let sn0: String = json_array_get(summary_nodes, 0)
|
||||
let sc0: String = json_get(sn0, "content")
|
||||
let ss0: String = if str_len(sc0) > 200 { str_slice(sc0, 0, 200) } else { sc0 }
|
||||
if str_eq(ss0, "") { sb } else { "- " + ss0 }
|
||||
} else { sb }
|
||||
let sb = if sn_total > 1 {
|
||||
let sn1: String = json_array_get(summary_nodes, 1)
|
||||
let sc1: String = json_get(sn1, "content")
|
||||
let ss1: String = if str_len(sc1) > 200 { str_slice(sc1, 0, 200) } else { sc1 }
|
||||
if str_eq(ss1, "") { sb } else { sb + "\n- " + ss1 }
|
||||
} else { sb }
|
||||
let sb = if sn_total > 2 {
|
||||
let sn2: String = json_array_get(summary_nodes, 2)
|
||||
let sc2: String = json_get(sn2, "content")
|
||||
let ss2: String = if str_len(sc2) > 200 { str_slice(sc2, 0, 200) } else { sc2 }
|
||||
if str_eq(ss2, "") { sb } else { sb + "\n- " + ss2 }
|
||||
} else { sb }
|
||||
sb
|
||||
} else { "" }
|
||||
|
||||
let hp: Bool = !str_eq(profile_bullets, "")
|
||||
let hw: Bool = !str_eq(work_bullets, "")
|
||||
let hpr: Bool = !str_eq(project_bullets, "")
|
||||
let hs: Bool = !str_eq(summary_bullet, "")
|
||||
let hs: Bool = !str_eq(summary_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 { "" }
|
||||
let sec_pr: String = if hpr { "[PROJECTS — from memory]\n" + project_bullets } else { "" }
|
||||
let sec_s: String = if hs { "[PREVIOUS SESSION — from memory]\n" + summary_bullet } else { "" }
|
||||
let sec_s: String = if hs { "[PREVIOUS SESSIONS]\n" + summary_bullets } else { "" }
|
||||
let sep1: String = if hp && (hw || hpr || hs) { "\n\n" } else { "" }
|
||||
let sep2: String = if hw && (hpr || hs) { "\n\n" } else { "" }
|
||||
let sep3: String = if hpr && hs { "\n\n" } else { "" }
|
||||
@@ -764,6 +821,31 @@ fn handle_chat(body: String) -> String {
|
||||
state_set("conv_history", final_hist)
|
||||
conv_history_persist(final_hist)
|
||||
|
||||
// Session-end summary hook: write a dated SessionSummary node once per boot when
|
||||
// the conversation reaches >= 5 user turns (10 hist entries = 5 user+assistant pairs).
|
||||
// Uses a per-boot label ("session:summary:<boot_ts>") so summaries accumulate across
|
||||
// sessions instead of overwriting a single global node. A state flag prevents rewriting
|
||||
// on every subsequent turn once the threshold is crossed.
|
||||
let final_hist_len: Int = json_array_len(final_hist)
|
||||
if final_hist_len >= 10 {
|
||||
let already_wrote: String = state_get("session_summary_written")
|
||||
if str_eq(already_wrote, "") {
|
||||
// Derive (or create) a stable boot-scoped session id.
|
||||
let boot_id: String = state_get("session_boot_id")
|
||||
let boot_id = if str_eq(boot_id, "") {
|
||||
let new_id: String = int_to_str(time_now())
|
||||
state_set("session_boot_id", new_id)
|
||||
new_id
|
||||
} else { boot_id }
|
||||
let sess_label: String = "session:summary:" + boot_id
|
||||
let auto_sum: String = session_summary_autogenerate(final_hist)
|
||||
if !str_eq(auto_sum, "") {
|
||||
let discard_sum: String = session_summary_write_dated(auto_sum, sess_label)
|
||||
state_set("session_summary_written", "1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 { "[]" }
|
||||
@@ -1863,3 +1945,55 @@ fn strengthen_chat_nodes(activation_nodes: String) -> Void {
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
// session_summary_autogenerate — build a minimal summary from conversation history without LLM.
|
||||
// Extracts user message snippets (first 80 chars each, up to 5 turns).
|
||||
// Called by the session-end hook when >= 5 complete turns have occurred.
|
||||
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
|
||||
}
|
||||
|
||||
// session_summary_write_dated — write a SessionSummary node with a caller-supplied dated label.
|
||||
// Unlike a global-label write, this does NOT delete old nodes — each session accumulates its
|
||||
// own node so engram_search_json("session:summary") can return multiple past sessions.
|
||||
// The label must be unique per session (e.g. "session:summary:<boot_ts>").
|
||||
fn session_summary_write_dated(summary_text: String, label: String) -> String {
|
||||
if str_eq(summary_text, "") { return "" }
|
||||
if str_eq(label, "") { 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
|
||||
let tags: String = "[\"SessionSummary\",\"session-summary\",\"previous-session\",\"consolidate\"]"
|
||||
let node_id: String = engram_node_full(
|
||||
content, "SessionSummary", label,
|
||||
el_from_float(0.9), el_from_float(0.8), el_from_float(1.0),
|
||||
"Episodic", tags
|
||||
)
|
||||
if str_eq(node_id, "") {
|
||||
println("[chat] session_summary_write_dated: engram write failed — summary node lost (label=" + label + ")")
|
||||
return ""
|
||||
}
|
||||
println("[chat] session_summary_write_dated: wrote SessionSummary (" + int_to_str(str_len(content)) + " chars) label=" + label + " -> " + node_id)
|
||||
return node_id
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user