diff --git a/chat.el b/chat.el index 595a5ad..7d022e5 100644 --- a/chat.el +++ b/chat.el @@ -29,9 +29,16 @@ fn engram_numeric_valid(s: String) -> Bool { let dot_count: Int = str_len(body) - str_len(no_dot) if dot_count > 1 { return false } if str_eq(no_dot, "") { return false } - // str_to_int on a letter-containing string returns 0; "0" is a valid zero. + // str_to_int on a letter-containing string returns 0; "0" and "00..." (e.g. from "0.0") + // are valid zeros. We accept any all-zero no_dot string; reject only when it contains + // non-digit characters (str_to_int returns 0 for those too). let parsed: Int = str_to_int(no_dot) - if parsed == 0 && !str_eq(no_dot, "0") { return false } + if parsed == 0 { + // Verify no_dot is truly all-digit-zeros, not a letter-contaminated string. + // Strip all '0' characters; if anything remains the string is non-numeric. + let stripped_zeros: String = str_replace(no_dot, "0", "") + if !str_eq(stripped_zeros, "") { return false } + } return true } @@ -180,10 +187,16 @@ fn engram_compile(intent: String) -> String { } else { println("[chat] engram_compile: WARN cold-index — activation and search returned no results for intent=" + str_slice(intent, 0, 60)) } - // Soul-agnostic fallback: search for Persona/Identity nodes by label. - let persona_fallback: String = engram_search_json("soul:persona Persona identity", 5) - let pf_ok: Bool = !str_eq(persona_fallback, "") && !str_eq(persona_fallback, "[]") - let combined: String = if pf_ok { engram_compile_ranked(persona_fallback, 3) } else { "" } + // Soul-agnostic fallback: fetch the Persona node by label — immune to cold vector index. + // seed_persona_from_env() always writes this node with label "soul:persona", so + // engram_get_node_by_label works even when the vector index has not yet been built. + // Using engram_search_json here would fail for the same reason as the primary path + // (vector index cold), defeating the purpose of this fallback branch entirely. + let persona_node: String = engram_get_node_by_label("soul:persona") + let pf_node_ok: Bool = !str_eq(persona_node, "") && !str_eq(persona_node, "null") + let persona_arr: String = if pf_node_ok { "[" + persona_node + "]" } else { "" } + let pf_ok: Bool = pf_node_ok + let combined: String = if pf_ok { engram_compile_ranked(persona_arr, 1) } else { "" } if str_eq(combined, "") { println("[chat] engram_compile: WARN cold-start fallback also empty — LLM has no episodic context") } @@ -431,9 +444,11 @@ fn conv_history_persist(hist: String) -> Void { if str_eq(hist, "") { return "" } if str_eq(hist, "[]") { return "" } // Partial-write guard: refuse to persist a blob that is not a complete JSON array. - // A truncated write starting with '[' but missing ']' would overwrite a good node. + // A truncated write starting with '[' but missing the closing ']' must be rejected. + // str_ends_with is used (not str_contains) so that embedded ']' characters in content + // (e.g. "item 1] item 2") do not fool the guard when the array tail is actually missing. if !str_starts_with(hist, "[") { return "" } - if !str_contains(hist, "]") { return "" } + if !str_ends_with(hist, "]") { return "" } let tags: String = "[\"conv-history\",\"persistent\"]" let node_id: String = engram_node_full( hist, "Conversation", "conv:history", @@ -455,7 +470,7 @@ fn conv_history_load() -> String { let label_ok: Bool = !str_eq(label_node, "") && !str_eq(label_node, "null") if label_ok { let label_content: String = json_get(label_node, "content") - let label_valid: Bool = str_starts_with(label_content, "[") && str_contains(label_content, "]") + let label_valid: Bool = str_starts_with(label_content, "[") && str_ends_with(label_content, "]") if label_valid { return label_content } @@ -471,8 +486,9 @@ fn conv_history_load() -> String { if str_eq(results, "[]") { return "" } let node: String = json_array_get(results, 0) let content: String = json_get(node, "content") - // Partial-write guard: require both '[' prefix AND ']' presence. - if !str_starts_with(content, "[") || !str_contains(content, "]") { + // Partial-write guard: require both '[' prefix AND closing ']' at the tail. + // str_ends_with guards against embedded ']' in content fooling the check. + if !str_starts_with(content, "[") || !str_ends_with(content, "]") { println("[chat] conv_history_load: vector search result content invalid — treating as first turn") state_set("conv_history_load_failed", "1") return "" @@ -1096,7 +1112,7 @@ fn handle_chat_agentic(body: String) -> String { if str_eq(screen_action, "hard_bell") { 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\":[]}" - + } let req_model: String = json_get(body, "model") let model: String = if str_eq(req_model, "") { chat_default_model() } else { req_model }