From c30cadde02c6f096fc1fb63873bd857d359b0539 Mon Sep 17 00:00:00 2001 From: Cameron Date: Fri, 15 May 2026 15:10:02 -0400 Subject: [PATCH] ai: fix UTF-8 byte-slice panics in insight_generator log/truncation paths Switch four `&s[..N]` / `&s[..s.len().min(N)]` sites to `chars().take(N).collect::()` so truncation lands on character boundaries instead of mid-codepoint. The agentic summary preview log was panicking when generated content hit an em-dash at byte 200; the few-shot passage cap, brief_json_args debug formatter, and a test assertion message had the same latent bug. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/ai/insight_generator.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/ai/insight_generator.rs b/src/ai/insight_generator.rs index db3fb20..2e2da33 100644 --- a/src/ai/insight_generator.rs +++ b/src/ai/insight_generator.rs @@ -1749,8 +1749,8 @@ Return ONLY the summary, nothing else."#, .iter() .enumerate() .map(|(i, c)| { - let trimmed = if c.len() > 1000 { - format!("{}…", &c[..1000]) + let trimmed = if c.chars().count() > 1000 { + format!("{}…", c.chars().take(1000).collect::()) } else { c.clone() }; @@ -3406,8 +3406,8 @@ Return ONLY the summary, nothing else."#, obj.iter() .map(|(k, v)| { let rendered = match v { - serde_json::Value::String(s) if s.len() > 40 => { - format!("\"{}...\"", &s[..40]) + serde_json::Value::String(s) if s.chars().count() > 40 => { + format!("\"{}...\"", s.chars().take(40).collect::()) } _ => v.to_string(), }; @@ -4088,10 +4088,11 @@ Return ONLY the summary, nothing else."#, let title = title_raw.trim().trim_matches('"').to_string(); log::info!("Agentic generated title: {}", title); + let summary_preview: String = final_content.chars().take(200).collect(); log::info!( "Agentic generated summary ({} chars): {}", final_content.len(), - &final_content[..final_content.len().min(200)] + summary_preview ); // 14. Serialize the full message history for training data @@ -4671,7 +4672,7 @@ mod tests { assert!( out.starts_with("You are a journal writer in first person, warm and reflective."), "custom prompt must lead the system content; got: {}", - &out[..out.len().min(200)], + out.chars().take(200).collect::(), ); assert!( !out.contains("personal photo memory assistant"),