feat(recall): cross-session-continuity
This commit is contained in:
@@ -924,6 +924,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 { "[]" }
|
||||
@@ -1375,7 +1383,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") {
|
||||
@@ -1450,6 +1460,24 @@ fn handle_chat_agentic(body: String) -> String {
|
||||
// Increased from 20 to 40 turns: consistent with handle_chat window expansion.
|
||||
let trimmed: String = if json_array_len(updated2) > 40 { 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 }
|
||||
|
||||
@@ -1903,6 +1931,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")
|
||||
|
||||
@@ -310,12 +310,36 @@ fn emit_session_start_event() -> Void {
|
||||
}
|
||||
let ts: Int = time_now()
|
||||
|
||||
// Load previous session summary at boot — stash in state for session_preload (issue #6).
|
||||
// Primary: label-based. Fallback: vector search. Logs it so continuity is auditable.
|
||||
let prev_sum_node: String = engram_get_node_by_label("session:summary")
|
||||
let prev_sum_ok: Bool = !str_eq(prev_sum_node, "") && !str_eq(prev_sum_node, "null")
|
||||
let prev_sum_content: String = if prev_sum_ok {
|
||||
json_get(prev_sum_node, "content")
|
||||
} else {
|
||||
let sum_search: String = engram_search_json("SessionSummary session:summary previous-session", 2)
|
||||
let sum_srch_ok: Bool = !str_eq(sum_search, "") && !str_eq(sum_search, "[]")
|
||||
if sum_srch_ok {
|
||||
let sn: String = json_array_get(sum_search, 0)
|
||||
let stype: String = json_get(sn, "node_type")
|
||||
let scontent: String = json_get(sn, "content")
|
||||
if str_eq(stype, "SessionSummary") && !str_eq(scontent, "") { scontent } 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\""
|
||||
+ ",\"boot\":" + boot_num
|
||||
+ ",\"cgi\":\"" + eff_cgi + "\""
|
||||
+ ",\"node_count\":" + int_to_str(node_ct)
|
||||
+ ",\"edge_count\":" + int_to_str(edge_ct)
|
||||
+ ",\"identity_loaded\":" + has_identity
|
||||
+ ",\"prev_session_summary_loaded\":" + has_prev_sum
|
||||
+ ",\"ts\":" + int_to_str(ts) + "}"
|
||||
|
||||
let tags: String = "[\"internal-state\",\"session-start\",\"InternalStateEvent\"]"
|
||||
@@ -324,7 +348,7 @@ fn emit_session_start_event() -> Void {
|
||||
el_from_float(0.9), el_from_float(0.9), el_from_float(1.0),
|
||||
"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.
|
||||
|
||||
Reference in New Issue
Block a user