diff --git a/chat.el b/chat.el index 0c65b87..af68bf3 100644 --- a/chat.el +++ b/chat.el @@ -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) @@ -627,11 +665,15 @@ fn handle_chat(body: String) -> String { } else { "" } let ctx: String = engram_compile(activation_seed) + // Read IDs published by engram_compile so session_preload can skip duplicate nodes. + // EL has no multiple return values; engram_compile writes its seen set to state. + let seen_ids: String = state_get("engram_compile_seen_ids") let system: String = affective_prefix + build_system_prompt(ctx) // Issue 9 fix: add project-specific and session-summary searches to session preload. // Old hardcoded "user profile" and "in_progress active project" miss project-specific // nodes stored under names like "Prism" unless those exact words appear in content. + // Dedup fix: skip any node whose ID already appeared in engram_compile's output. let session_preload: String = if hist_len == 0 { let profile_nodes: String = engram_search_json("user profile identity preferences", 5) let work_nodes: String = engram_search_json("in_progress active project work", 5) @@ -648,21 +690,24 @@ fn handle_chat(body: String) -> String { let bullets: String = "" let bullets = if pn > 0 { let n0: String = json_array_get(profile_nodes, 0) + let n0_id: String = json_get(n0, "id") let c0: String = json_get(n0, "content") let s0: String = if str_len(c0) > 120 { str_slice(c0, 0, 120) } else { c0 } - if str_eq(s0, "") { bullets } else { "- " + s0 } + if str_eq(s0, "") || id_in_seen(n0_id, seen_ids) { bullets } else { "- " + s0 } } else { bullets } let bullets = if pn > 1 { let n1: String = json_array_get(profile_nodes, 1) + let n1_id: String = json_get(n1, "id") 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 } + if str_eq(s1, "") || id_in_seen(n1_id, seen_ids) { bullets } else { bullets + "\n- " + s1 } } else { bullets } let bullets = if pn > 2 { let n2: String = json_array_get(profile_nodes, 2) + let n2_id: String = json_get(n2, "id") 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 } + if str_eq(s2, "") || id_in_seen(n2_id, seen_ids) { bullets } else { bullets + "\n- " + s2 } } else { bullets } bullets } else { "" } @@ -672,15 +717,17 @@ fn handle_chat(body: String) -> String { let wb: String = "" let wb = if wn > 0 { let w0: String = json_array_get(work_nodes, 0) + let w0_id: String = json_get(w0, "id") let wc0: String = json_get(w0, "content") let ws0: String = if str_len(wc0) > 120 { str_slice(wc0, 0, 120) } else { wc0 } - if str_eq(ws0, "") { wb } else { "- " + ws0 } + if str_eq(ws0, "") || id_in_seen(w0_id, seen_ids) { wb } else { "- " + ws0 } } else { wb } let wb = if wn > 1 { let w1: String = json_array_get(work_nodes, 1) + let w1_id: String = json_get(w1, "id") let wc1: String = json_get(w1, "content") let ws1: String = if str_len(wc1) > 120 { str_slice(wc1, 0, 120) } else { wc1 } - if str_eq(ws1, "") { wb } else { wb + "\n- " + ws1 } + if str_eq(ws1, "") || id_in_seen(w1_id, seen_ids) { wb } else { wb + "\n- " + ws1 } } else { wb } wb } else { "" } @@ -690,24 +737,27 @@ fn handle_chat(body: String) -> String { let pb: String = "" let pb = if prn > 0 { let pr0: String = json_array_get(project_nodes, 0) + let pr0_id: String = json_get(pr0, "id") let prc0: String = json_get(pr0, "content") let ps0: String = if str_len(prc0) > 120 { str_slice(prc0, 0, 120) } else { prc0 } - if str_eq(ps0, "") { pb } else { "- " + ps0 } + if str_eq(ps0, "") || id_in_seen(pr0_id, seen_ids) { pb } else { "- " + ps0 } } else { pb } let pb = if prn > 1 { let pr1: String = json_array_get(project_nodes, 1) + let pr1_id: String = json_get(pr1, "id") 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 } + if str_eq(ps1, "") || id_in_seen(pr1_id, seen_ids) { pb } else { pb + "\n- " + ps1 } } else { pb } pb } else { "" } let summary_bullet: String = if summary_ok { let sn0: String = json_array_get(summary_nodes, 0) + let sn0_id: String = json_get(sn0, "id") 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 } + if str_eq(ss0, "") || id_in_seen(sn0_id, seen_ids) { "" } else { "- " + ss0 } } else { "" } let hp: Bool = !str_eq(profile_bullets, "")