From f969de91aafdd013bc7ecc682fee26e88f85984a Mon Sep 17 00:00:00 2001 From: Friende <35026241+pengyou200902@users.noreply.github.com> Date: Fri, 8 May 2026 02:55:30 -0400 Subject: [PATCH] fix(memory): report omitted bytes in truncation marker --- crates/tui/src/memory.rs | 54 ++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/crates/tui/src/memory.rs b/crates/tui/src/memory.rs index fa7da78d..92fa7396 100644 --- a/crates/tui/src/memory.rs +++ b/crates/tui/src/memory.rs @@ -28,8 +28,9 @@ use std::path::Path; use chrono::Utc; /// Maximum size of the user memory file. Larger files are loaded but the -/// `` block carries a "(truncated)" marker so the user knows -/// the model only saw a slice. Mirrors `project_context::MAX_CONTEXT_SIZE`. +/// `` block carries a `` +/// marker so the user knows the model only saw a slice. Mirrors +/// `project_context::MAX_CONTEXT_SIZE`. const MAX_MEMORY_SIZE: usize = 100 * 1024; /// Read the user memory file at `path`, returning `None` when the file @@ -54,11 +55,12 @@ pub fn as_system_block(content: &str, source: &Path) -> Option { return None; } - let display = source.display(); + let display = source.display().to_string(); let payload = if content.len() > MAX_MEMORY_SIZE { - let cutoff = previous_char_boundary(content, MAX_MEMORY_SIZE); + let cutoff = truncation_cutoff(content, &display); + let omitted_bytes = content.len() - cutoff; let mut head = content[..cutoff].to_string(); - head.push_str("\n…(truncated, raise [memory].max_size or trim memory.md)"); + head.push_str(&truncation_marker(omitted_bytes, &display)); head } else { trimmed.to_string() @@ -69,6 +71,24 @@ pub fn as_system_block(content: &str, source: &Path) -> Option { )) } +fn truncation_cutoff(content: &str, source: &str) -> usize { + let mut cutoff = previous_char_boundary(content, MAX_MEMORY_SIZE); + loop { + let omitted_bytes = content.len() - cutoff; + let max_head_len = + MAX_MEMORY_SIZE.saturating_sub(truncation_marker(omitted_bytes, source).len()); + let next_cutoff = previous_char_boundary(content, cutoff.min(max_head_len)); + if next_cutoff == cutoff { + return cutoff; + } + cutoff = next_cutoff; + } +} + +fn truncation_marker(omitted_bytes: usize, source: &str) -> String { + format!("\n") +} + fn previous_char_boundary(value: &str, mut index: usize) -> usize { while !value.is_char_boundary(index) { index -= 1; @@ -166,7 +186,9 @@ mod tests { fn as_system_block_truncates_oversize_input() { let big = "x".repeat(MAX_MEMORY_SIZE + 100); let block = as_system_block(&big, Path::new("/tmp/m.md")).unwrap(); - assert!(block.contains("(truncated")); + let payload = user_memory_payload(&block); + assert_eq!(payload.len(), MAX_MEMORY_SIZE); + assert!(payload.ends_with("")); } #[test] @@ -182,10 +204,11 @@ mod tests { .strip_suffix("\n") .unwrap(); let (head, marker) = payload - .split_once("\n…(truncated, raise [memory].max_size or trim memory.md)") + .split_once("\n") .unwrap(); - assert_eq!(head.len(), MAX_MEMORY_SIZE - 1); + assert_eq!(payload.len(), MAX_MEMORY_SIZE); + assert_eq!(head.len(), MAX_MEMORY_SIZE - 40); assert!(head.bytes().all(|byte| byte == b'x')); assert_eq!(marker, ""); } @@ -197,7 +220,7 @@ mod tests { content.push_str("tail"); let block = as_system_block(&content, Path::new("/tmp/m.md")).unwrap(); - assert!(block.contains("…(truncated, raise [memory].max_size or trim memory.md)")); + assert!(block.contains("")); let payload = block .strip_prefix("\n") @@ -205,14 +228,23 @@ mod tests { .strip_suffix("\n") .unwrap(); let head = payload - .strip_suffix("\n…(truncated, raise [memory].max_size or trim memory.md)") + .strip_suffix("\n") .unwrap(); + assert_eq!(payload.len(), MAX_MEMORY_SIZE); assert!(head.len() <= MAX_MEMORY_SIZE); - assert_eq!(head.len(), MAX_MEMORY_SIZE - 1); + assert_eq!(head.len(), MAX_MEMORY_SIZE - 40); assert!(head.bytes().all(|byte| byte == b'x')); } + fn user_memory_payload(block: &str) -> &str { + block + .strip_prefix("\n") + .unwrap() + .strip_suffix("\n") + .unwrap() + } + #[test] fn append_entry_creates_file_and_writes_one_bullet() { let tmp = tempdir().unwrap();