Skip to content

fix(tool_notifier): use char-safe truncation to prevent UTF-8 boundary panic#29

Open
chinkan wants to merge 30 commits intomainfrom
claude/chat-history-rag-telegram-T4Jmo
Open

fix(tool_notifier): use char-safe truncation to prevent UTF-8 boundary panic#29
chinkan wants to merge 30 commits intomainfrom
claude/chat-history-rag-telegram-T4Jmo

Conversation

@chinkan
Copy link
Owner

@chinkan chinkan commented Mar 17, 2026

Summary

  • Fixed a production crash in format_args_preview (src/platform/tool_notifier.rs) caused by byte-indexed string slicing (&s[..60]) panicking when byte 60 falls inside a multi-byte UTF-8 character (e.g. Chinese = bytes 58–61)
  • Replaced both unsafe byte-slices (single-arg fast-path and raw-JSON fallback) with a truncate_chars() helper that walks .chars() to find the safe byte boundary before slicing
  • Added two regression tests: one reproducing the exact production JSON shape that crashed the bot

Root Cause

String::len() returns byte length, not char count. The schedule_task args JSON contained Chinese characters; the multi-arg fallback path hit &args_json[..60] with byte 60 in the middle of , causing a panic → tokio worker death → TX is dead cascade crash.

Test Plan

  • test_format_args_preview_multi_arg_chinese_panics_before_fix — reproduces the exact production crash input, now passes after fix
  • test_format_args_preview_single_arg_with_chinese — exercises single-arg path with Chinese string
  • All 103 tests pass (cargo test)
  • cargo clippy -- -D warnings clean
  • cargo fmt --check clean

🤖 Generated with Claude Code

claude and others added 26 commits March 14, 2026 05:23
… tool call UI

Covers three features approved for implementation:
1. Framework auto-inject of semantically relevant past messages via <retrieved_context>
2. Nightly cron summarization storing [SUMMARY] system messages in SQLite
3. Live-edited Telegram status message showing active tool calls (/verbose toggle)

https://claude.ai/code/session_0144HmaaG5wwqmBkBYhtGBgh
…d tool call UI

7-task TDD plan covering:
- DB migration (is_summarized column, conversation-scoped search)
- memory/rag.rs (auto_retrieve_context with <retrieved_context> injection)
- memory/summarizer.rs (nightly LLM-based summarization via cron)
- platform/tool_notifier.rs (live-editing Telegram progress message)
- /verbose bot command (per-user toggle stored in knowledge table)
- Enhanced system prompt for small model compatibility

https://claude.ai/code/session_0144HmaaG5wwqmBkBYhtGBgh
Two features now in scope:
1. Query rewriting (memory/query_rewriter.rs) — rewrites ambiguous follow-ups
   into standalone queries before RAG vector search; falls back gracefully
2. LLM response streaming — final text response streamed token-by-token to
   Telegram via reqwest SSE + edit_message_text; tool iterations stay blocking

https://claude.ai/code/session_0144HmaaG5wwqmBkBYhtGBgh
…s 8-13)

Extends the original 7-task plan with:
- Task 8: memory/query_rewriter.rs with rewrite_for_rag() + 5 unit tests
- Task 9: wire rewriter into auto_retrieve_context() (new llm + recent_messages params)
- Task 10: llm.rs chat_stream() with SSE parsing, StreamRequest, parse_sse_content()
- Task 11: process_message() gains stream_token_tx param; final response chunked to channel
- Task 12: Telegram spawns streaming receiver task with 500ms edit batching + 3800-char split
- Task 13: final verification (test/clippy/release build/push)

https://claude.ai/code/session_0144HmaaG5wwqmBkBYhtGBgh
…m prompt each turn

- Add src/memory/rag.rs with auto_retrieve_context(): skips short queries (<5 chars) and /commands, returns <retrieved_context> block with snippets capped at 300 chars
- Register pub mod rag in src/memory/mod.rs
- Add rag_limit, max_raw_messages, summarize_threshold, summarize_cron to MemoryConfig with defaults (5, 50, 20, "0 0 2 * * *")
- Wire RAG into agent.rs: inject context block into system prompt after refresh but before user message; use load_messages_with_limit with config.memory.max_raw_messages
- Suppress dead_code warnings for load_messages() and future-use config fields

https://claude.ai/code/session_0144HmaaG5wwqmBkBYhtGBgh
…on job

Adds summarize_conversation() and summarize_all_active() in memory/summarizer.rs,
registers a configurable nightly cron job in scheduler/tasks.rs, and wires the
LlmClient + MemoryConfig fields through register_builtin_tasks in main.rs.

https://claude.ai/code/session_0144HmaaG5wwqmBkBYhtGBgh
- Add get_unsummarized_messages(), mark_messages_summarized(), get_active_conversations() to MemoryStore
- Add #[derive(Clone)] to LlmClient (required for nightly summarization cron closure)

https://claude.ai/code/session_0144HmaaG5wwqmBkBYhtGBgh
…l infrastructure

- New src/platform/tool_notifier.rs: ToolEvent enum, format_args_preview(), ToolCallNotifier struct with start/handle_event/finish methods and rate-limited Telegram edits
- Register pub mod tool_notifier in platform/mod.rs
- Add tool_event_tx: Option<mpsc::Sender<ToolEvent>> param to agent process_message()
- Emit ToolEvent::Started/Completed around each tool execution in agent loop
- Update all callers (telegram.rs, main.rs) to pass None (Task 5 will wire the real channel)

https://claude.ai/code/session_0144HmaaG5wwqmBkBYhtGBgh
…otifier into agentic loop

- Add /verbose toggle command: reads/writes tool_ui_enabled_{user_id} in knowledge store
- Add is_verbose_enabled() helper + test
- When verbose on: create mpsc channel, spawn ToolCallNotifier task, pass sender to process_message
- process_message drops sender on return, notifier task exits cleanly via channel close
- Update /start message to list /verbose command

https://claude.ai/code/session_0144HmaaG5wwqmBkBYhtGBgh
…otifier into agentic loop

Adds is_verbose_enabled() helper with unit test, /verbose toggle command that
persists per-user setting in memory, and wires ToolCallNotifier into process_message
via mpsc channel when verbose mode is enabled. Also fixes formatting across several
files and a redundant_closure clippy warning in memory/conversations.rs.

https://claude.ai/code/session_0144HmaaG5wwqmBkBYhtGBgh
…follow-ups before vector search

- Update auto_retrieve_context() signature: add llm: Option<&LlmClient> and recent_messages params
- When llm provided, call rewrite_for_rag() to resolve pronouns before FTS/vector search
- Pass llm and last 6 user/assistant messages from agent.rs call site
- Update all existing rag.rs tests to pass None/&[] for new params

https://claude.ai/code/session_0144HmaaG5wwqmBkBYhtGBgh
…aming via mpsc channel

- Add reqwest stream feature to Cargo.toml
- Add StreamRequest struct with stream=true field
- Add parse_sse_content() to extract tokens from OpenRouter SSE data lines
- Add chat_stream() method to LlmClient: streams response tokens via mpsc::Sender<String>
- 6 new unit tests for SSE parsing and StreamRequest serialization

https://claude.ai/code/session_0144HmaaG5wwqmBkBYhtGBgh
…esponse as word-chunks via mpsc

- Add stream_token_tx: Option<mpsc::Sender<String>> param to process_message()
- Stream final text response as 4-word chunks with 30ms delay for typing effect
- Add test_assemble_tokens_joins_correctly to document assembly contract
- Update all callers (main.rs, telegram.rs) to pass None (Task 12 will wire real channel)

https://claude.ai/code/session_0144HmaaG5wwqmBkBYhtGBgh
…egram message as LLM tokens arrive

- Spawn stream receiver task with zero-width space placeholder message
- Receive word-chunk tokens from process_message via mpsc channel
- Edit Telegram message at most every 500ms (rate limit)
- Split into new message when buffer exceeds 3800 chars
- stream_handle.await after process_message for final edit to complete
- Error path: abort stream_handle, send error as new message
- Fix formatting across llm.rs, rag.rs, query_rewriter.rs

https://claude.ai/code/session_0144HmaaG5wwqmBkBYhtGBgh
…eam_handle

Telegram rejects messages containing only zero-width space (U+200B),
causing stream_handle to return early and drop the receiver. This made
every tx.send() in agent.rs fail, breaking the streaming loop so
process_message returned Ok while nothing was ever delivered to the user.

Remove the placeholder send. Instead, accumulate tokens and:
- Send the first real message after 500ms of content (or at channel close).
- Edit that message for subsequent updates.
- Fall back to a direct send_message at the end if no intermediate
  message was sent (covers short responses < 500ms token delivery).

Errors on send are now logged via tracing::error/warn instead of
being silently swallowed.

https://claude.ai/code/session_0144HmaaG5wwqmBkBYhtGBgh
…essage

Root cause: ToolCallNotifier::finish() unconditionally called delete_message(),
erasing all evidence of tool calls from the Telegram chat. After the response
arrived the user saw no record of which tools were used — no tool UI.

Fix: when tools were actually called, edit the status message to a final
persistent summary ("🔧 Tools used:\n✅ tool(args)") instead of deleting it.
When no tools ran, delete the "⏳ Working..." placeholder as before (not useful).

Adds test_format_final_includes_all_tools to verify the new format.

https://claude.ai/code/session_0144HmaaG5wwqmBkBYhtGBgh
Two tests reproduce the production panic in format_args_preview when
tool arguments contain Chinese/multibyte characters and byte-indexed
slicing lands mid-character. These tests are intentionally failing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tion tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…y panic

Byte-indexed slicing (&s[..60]) panics when the byte boundary falls
inside a multi-byte character (e.g. Chinese 香 = 3 bytes at 58..61).
Replace with truncate_chars() which walks .chars() to find the safe
byte boundary, used in both the single-arg fast-path and the raw-JSON
fallback inside format_args_preview.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link

github-actions bot commented Mar 17, 2026

Claude finished @chinkan's task in 1m 51s —— View job


Code Review: PR #29 — fix(tool_notifier): use char-safe truncation to prevent UTF-8 boundary panic

Tasks:

  • Gather context & understand request
  • Read changed source files
  • Review core fix (src/platform/tool_notifier.rs)
  • Review supporting changes (memory, agent, config, llm, telegram)
  • Post review feedback

Overview

The stated fix (char-safe truncation in tool_notifier.rs) is correct and well-implemented. However, this PR is substantially larger than its title suggests — it introduces a full RAG/memory system (vector search, FTS5, summarization, query rewriting, streaming Telegram UI) across 20 files with 5,153 additions. The review covers both the fix and the new code.


✅ The Core Fix (tool_notifier.rs)

The truncate_chars() helper is correct and idiomatic Rust:

fn truncate_chars(s: &str, max_chars: usize) -> String {
    let mut byte_end = 0usize;
    for (char_count, ch) in s.chars().enumerate() {
        if char_count == max_chars {
            return format!("{}...", &s[..byte_end]);
        }
        byte_end += ch.len_utf8();
    }
    s.to_string()
}
  • Iterates chars to find a safe byte boundary before slicing ✓
  • Appends "..." only when truncation actually occurs ✓
  • Regression tests reproduce the exact production crash input ✓

🔴 Critical: Same Bug Exists in Two New Files

The identical byte-indexing pattern that caused the original crash has been introduced in two new files:

src/memory/rag.rs:45–47Fix this →

let snippet = if content.len() > 300 {
    format!("{}...", &content[..300])  // 🔴 panics if byte 300 is mid-char

src/memory/query_rewriter.rs:97–99 — same issue:

let snippet = if c.len() > 200 {
    format!("{}...", &c[..200])  // 🔴 panics if byte 200 is mid-char

Both paths will be hit with Chinese/Japanese/emoji content (the exact scenario that caused the original crash). The truncate_chars helper from tool_notifier.rs should be extracted to a shared utility and reused here.


🟡 Major: PR Scope vs. Title Mismatch

The PR title reads fix(tool_notifier): use char-safe truncation to prevent UTF-8 boundary panic, but the diff is 5,153 additions across 20 files. The actual content includes:

  • Full SQLite-backed memory store with hybrid vector + FTS5 search
  • LLM-driven query rewriting for RAG
  • Conversation summarization pipeline
  • Streaming Telegram UI with progressive message editing
  • ToolCallNotifier with live-updating status messages
  • Agent-level RAG injection into system prompts

This makes the PR very hard to review: the tiny UTF-8 fix is lost inside a large feature branch. Consider splitting into focused PRs, or at minimum renaming the title/description to reflect the actual scope.


🟡 Medium: mark_messages_summarized — N+1 Queries Without Transaction

src/memory/conversations.rs:376–389:

for id in message_ids {
    conn.execute("UPDATE messages SET is_summarized = 1 WHERE id = ?1", ...)?;
}

For large conversations this issues N separate UPDATE statements with no wrapping transaction. SQLite commits after each statement by default, making this O(N) FSYNCs. Should use a single transaction or WHERE id IN (...).


🟡 Medium: Duplicated unsafe sqlite-vec Registration

src/memory/mod.rs:29–41 and 68–80 — the identical unsafe { sqlite3_auto_extension(...) } block appears in both open() and open_in_memory(). sqlite3_auto_extension is safe to call multiple times but registering the same extension twice is wasteful. Extract to a fn register_vec_extension() called once (e.g., via std::sync::Once).


🟡 Medium: Weak Test Assertion

src/platform/tool_notifier.rs:194:

assert!(preview.len() <= 70, "Preview should be truncated");

This asserts byte length ≤ 70, but truncate_chars limits to 60 characters (plus 3 for "..."). For ASCII this holds, but the assertion doesn't catch a regression where truncation stops at 65 chars (still ≤ 70 bytes). Change to assert exact char count:

let char_count = preview.trim_matches('"').chars().count();
assert!(char_count <= 63); // 60 + "..."

🟢 Minor / Good Practices

  • ToolCallNotifier rate-limiting (tool_notifier.rs:113–117): async sleep for Telegram edit-rate compliance is correct. Note: handle_eventedit_message blocks until the sleep completes; if you fire many Started events quickly you may stall the notifier goroutine for up to 1 second per event. A debounce/batching approach would be more responsive, but it's fine for current usage.

  • split_message test-only: telegram.rs:12 marks split_message as #[cfg(test)], which means the function exists only in tests. If message splitting is needed at runtime (e.g., for very long non-streamed responses), this should be un-gated.

  • #[allow(dead_code)] prevalence: ToolEvent, ToolCallNotifier, load_messages, search_messages, auto_retrieve_context and rewrite_for_rag all carry #[allow(dead_code)]. New public-facing code with suppressed warnings usually indicates it isn't yet wired into the call graph — consider tracking integration as a follow-up task.

  • f32_slice_to_bytes alignment (conversations.rs:8–9): The unsafe cast from &[f32] to &[u8] is technically sound on x86/ARM (little-endian) but silently produces wrong vector distances on big-endian platforms. A clarifying comment documenting the endianness assumption would help future maintainers.


Summary

Severity Issue
🔴 Critical Same byte-unsafe truncation in rag.rs:45 and query_rewriter.rs:97
🟡 Major PR title/description doesn't reflect actual scope
🟡 Medium mark_messages_summarized: N+1 writes without transaction
🟡 Medium Duplicated unsafe sqlite-vec registration block
🟡 Medium Weak byte-length assertion in truncation test
🟢 Minor split_message only exists in test context
🟢 Minor Widespread #[allow(dead_code)] on new public code
🟢 Minor Endianness assumption undocumented in f32_slice_to_bytes

The critical items (rag.rs / query_rewriter.rs truncation) should be fixed before merging, as they reintroduce the exact class of panic this PR was written to eliminate.

@github-actions
Copy link

github-actions bot commented Mar 17, 2026

Claude finished @chinkan's task in 1m 31s —— View job


Code Review: PR #29 — fix(tool_notifier): use char-safe truncation to prevent UTF-8 boundary panic

Tasks:

  • Gather context & understand request
  • Read changed source files (rag.rs, query_rewriter.rs, tool_notifier.rs, conversations.rs, mod.rs)
  • Check if previous critical issues were addressed
  • Review new/changed code
  • Post review feedback

Status Since Last Review

This review was triggered by a new push to the branch. The previous review identified two critical byte-unsafe truncation bugs introduced alongside the UTF-8 fix. Both remain unaddressed.


✅ The Core Fix (tool_notifier.rs)

The truncate_chars() helper is correct and idiomatic:

fn truncate_chars(s: &str, max_chars: usize) -> String {
    let mut byte_end = 0usize;
    for (char_count, ch) in s.chars().enumerate() {
        if char_count == max_chars {
            return format!("{}...", &s[..byte_end]);
        }
        byte_end += ch.len_utf8();
    }
    s.to_string()
}

Both call sites in format_args_preview correctly use truncate_chars. Regression tests pass. ✓


🔴 Critical: Same Bug Still Present in Two New Files

The exact byte-indexing panic that this PR was written to fix has been reintroduced in two files and has not been fixed after the previous review pointed them out.

src/memory/rag.rs:45–46Fix this →

// src/memory/rag.rs:45-46
let snippet = if content.len() > 300 {
    format!("{}...", &content[..300])  // 🔴 panics if byte 300 is mid-char

src/memory/query_rewriter.rs:97–98 — same pattern:

// src/memory/query_rewriter.rs:97-98
let snippet = if c.len() > 200 {
    format!("{}...", &c[..200])  // 🔴 panics if byte 200 is mid-char

Both paths are exercised with Chinese/Japanese input (the exact scenario that caused the original crash). The test_format_history_truncates_long_content test in query_rewriter.rs:184–194 only uses ASCII "x" — it will not catch this regression.

The fix: extract truncate_chars to a shared utility (e.g. src/utils/str.rs) and replace both byte-slices:

// rag.rs
let snippet = crate::utils::str::truncate_chars(content, 300);

// query_rewriter.rs
let snippet = crate::utils::str::truncate_chars(c, 200);

🟡 Medium: mark_messages_summarized — N+1 Writes Without Transaction

src/memory/conversations.rs:376–389:

for id in message_ids {
    conn.execute("UPDATE messages SET is_summarized = 1 WHERE id = ?1", ...)?;
}

Each iteration issues a separate SQLite UPDATE with an implicit FSYNC. For a conversation with 50+ messages this is 50 separate disk commits. Should be wrapped in a single transaction:

conn.execute_batch("BEGIN;")?;
for id in message_ids { ... }
conn.execute_batch("COMMIT;")?;

Or replaced with a single WHERE id IN (...) query.


🟡 Medium: Duplicated unsafe sqlite-vec Registration

src/memory/mod.rs:29–41 and 68–80 — the identical unsafe { sqlite3_auto_extension(...) } block appears in both open() and open_in_memory(). sqlite3_auto_extension registers a process-global hook; calling it twice is harmless but wasteful and adds maintenance risk. Extract to a helper called via std::sync::Once:

fn ensure_vec_extension_registered() {
    static ONCE: std::sync::Once = std::sync::Once::new();
    ONCE.call_once(|| unsafe { /* registration */ });
}

🟡 Medium: Weak Test Assertion for Truncation

src/platform/tool_notifier.rs:194:

assert!(preview.len() <= 70, "Preview should be truncated");

preview.len() is byte length. truncate_chars(s, 60) produces at most 60 chars + "..." = 63 chars. For ASCII a × 100, that's 63 bytes — well within 70. But the test won't catch a regression where truncation stops at, say, 65 chars (still ≤ 70 bytes). Prefer:

// strip surrounding quotes added by format_args_preview
let inner = preview.trim_matches('"').trim_end_matches("\"");
let char_count = inner.replace("...", "").chars().count();
assert!(char_count <= 60);

🟢 Minor: Endianness Assumption Undocumented in f32_slice_to_bytes

src/memory/conversations.rs — the unsafe cast from &[f32] to &[u8] for vector storage is correct on little-endian platforms (x86, ARM) but will silently produce wrong cosine-distance results on big-endian. A comment documenting this assumption costs nothing and prevents a future maintainer from getting confused on an exotic target.


🟢 Minor: Widespread #[allow(dead_code)] on Public API

ToolEvent, ToolCallNotifier, auto_retrieve_context, rewrite_for_rag all carry #[allow(dead_code)]. This is fine for work-in-progress, but these suppressions should be removed once the code is wired into the call graph — otherwise Clippy's dead-code lint no longer signals when something is genuinely unused.


Summary

Severity Issue Status
🔴 Critical Byte-unsafe truncation in rag.rs:45 and query_rewriter.rs:97 Not fixed
🟡 Medium mark_messages_summarized: N+1 writes without transaction Not fixed
🟡 Medium Duplicated unsafe sqlite-vec registration block Not fixed
🟡 Medium Weak byte-length assertion in truncation test Not fixed
🟢 Minor Endianness assumption undocumented in f32_slice_to_bytes Not fixed
🟢 Minor Widespread #[allow(dead_code)] on new public code Not fixed

The two critical items in rag.rs and query_rewriter.rs must be fixed before merging — they reintroduce the exact class of UTF-8 boundary panic this PR was written to eliminate.

Move the UTF-8-safe string truncation helper from tool_notifier.rs
into a new src/utils/str module so rag.rs and query_rewriter.rs
can reuse it without duplicating the implementation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
chinkan and others added 2 commits March 18, 2026 00:56
&content[..300] panics when byte 300 falls inside a multi-byte char.
Chinese/Japanese input in retrieved context triggered this path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t UTF-8 panic

&c[..200] panics when byte 200 falls inside a multi-byte character.
Also adds a multibyte regression test — the existing test only used ASCII.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link

github-actions bot commented Mar 17, 2026

Claude finished @chinkan's task in 1m 32s —— View job


Code Review: PR #29 — fix(tool_notifier): use char-safe truncation to prevent UTF-8 boundary panic

Tasks:

  • Gather context & understand request
  • Read changed source files (rag.rs, query_rewriter.rs, tool_notifier.rs, conversations.rs, mod.rs, utils/str.rs)
  • Check if previous critical issues were addressed
  • Review new/changed code
  • Post review feedback

Status Since Last Review

Good progress: both critical UTF-8 fixes from the previous two review rounds have been applied. The remaining items are medium/minor.


✅ Critical Issues — Resolved

src/memory/rag.rs:45 — Fixed ✓

let snippet = crate::utils::str::truncate_chars(content, 300);

src/memory/query_rewriter.rs:97 — Fixed ✓

let snippet = crate::utils::str::truncate_chars(c, 200);

Both sites now use the shared truncate_chars helper from src/utils/str.rs, and each has a Chinese-input regression test (test_snippet_truncation_chinese_no_panic, test_format_history_truncates_long_chinese_no_panic). The class of panic that motivated this PR cannot recur at these call sites. ✓


🟡 Medium: mark_messages_summarized — N+1 Writes Still Without Transaction

src/memory/conversations.rs:381–387 — unchanged from previous reviews:

for id in message_ids {
    conn.execute(
        "UPDATE messages SET is_summarized = 1 WHERE id = ?1",
        rusqlite::params![id],
    )?;
}

SQLite commits after each statement by default, meaning N messages → N implicit FSYNCs. For conversations summarized before the context window fills (could be 50+ messages), this is a meaningful performance issue. Fix this →

Alternatively, a single WHERE id IN (...) query avoids the loop entirely.


🟡 Medium: Duplicated unsafe sqlite-vec Registration

src/memory/mod.rs:30–41 and 69–80 — identical unsafe blocks in open() and open_in_memory(), still unchanged:

unsafe {
    type VecInitFn = ...;
    rusqlite::ffi::sqlite3_auto_extension(Some(std::mem::transmute::<...>(...)));
}

sqlite3_auto_extension registers a process-global hook. Calling it twice is harmless but the duplication creates a maintenance hazard (two sites to update if the registration logic changes). Fix this →


🟡 Medium: Weak Byte-Length Assertion in Truncation Test

src/platform/tool_notifier.rs:180 — still present:

assert!(preview.len() <= 70, "Preview should be truncated");

preview.len() is byte count. truncate_chars(s, 60) produces at most 60 chars + "..." = 63 chars. For ASCII input this happens to pass, but the bound (70) is loose enough that a regression truncating at 65 chars would still pass. The assertion should verify character count, not byte count:

// strip surrounding quotes, then count chars before "..."
let inner = preview.trim_matches('"');
let content_chars = inner.trim_end_matches("...").chars().count();
assert!(content_chars <= 60, "Expected ≤60 chars, got {}", content_chars);

Fix this →


🟢 Minor: split_message Test-Only Annotation — Now Consistent

telegram.rs:11–12 marks split_message as #[cfg(test)]. The runtime now uses streaming instead of post-hoc splitting, so this is intentional and correct — no longer flagged. ✓


🟢 Minor: f32_slice_to_bytes — Endianness Assumption Undocumented

src/memory/conversations.rs:8–9:

pub(crate) fn f32_slice_to_bytes(floats: &[f32]) -> &[u8] {
    unsafe { std::slice::from_raw_parts(floats.as_ptr() as *const u8, floats.len() * 4) }
}

The cast is technically correct on little-endian platforms (x86, ARM). On big-endian it silently produces wrong cosine distances. A // SAFETY: comment noting the little-endian assumption costs nothing and prevents future confusion.


🟢 Minor: #[allow(dead_code)] on New Public API

ToolEvent, ToolCallNotifier, load_messages, search_messages, auto_retrieve_context, and rewrite_for_rag all carry #[allow(dead_code)]. This is expected for work-in-progress. These suppressions should be removed once the code is wired into the call graph so Clippy's dead-code lint can signal genuine staleness.


Summary

Severity Issue Status
🔴 Critical Byte-unsafe truncation in rag.rs and query_rewriter.rs Fixed ✅
🟡 Medium mark_messages_summarized: N+1 writes without transaction Not fixed
🟡 Medium Duplicated unsafe sqlite-vec registration block Not fixed
🟡 Medium Weak byte-length assertion in truncation test Not fixed
🟢 Minor split_message test-only — now intentional with streaming Resolved ✅
🟢 Minor Endianness assumption undocumented in f32_slice_to_bytes Not fixed
🟢 Minor #[allow(dead_code)] on new public code Not fixed (acceptable for WIP)

The critical blocking items are resolved. The remaining medium items (N+1 writes, duplicated unsafe block, weak assertion) are worth addressing before merge but are not safety/correctness blockers in the same class as the UTF-8 panic. The PR is significantly cleaner than when it started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants