Conversation
… 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
…, raw message limit in load_messages 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
…ing split threshold
…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
…ag-telegram-T4Jmo
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>
|
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 panicTasks:
OverviewThe stated fix (char-safe truncation in ✅ The Core Fix (tool_notifier.rs)The 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()
}
🔴 Critical: Same Bug Exists in Two New FilesThe identical byte-indexing pattern that caused the original crash has been introduced in two new files:
let snippet = if content.len() > 300 {
format!("{}...", &content[..300]) // 🔴 panics if byte 300 is mid-char
let snippet = if c.len() > 200 {
format!("{}...", &c[..200]) // 🔴 panics if byte 200 is mid-charBoth paths will be hit with Chinese/Japanese/emoji content (the exact scenario that caused the original crash). The 🟡 Major: PR Scope vs. Title MismatchThe PR title reads
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:
|
| 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.
… and verbose tool UI features
|
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 panicTasks:
Status Since Last ReviewThis 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 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 🔴 Critical: Same Bug Still Present in Two New FilesThe 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-46
let snippet = if content.len() > 300 {
format!("{}...", &content[..300]) // 🔴 panics if byte 300 is mid-char
// src/memory/query_rewriter.rs:97-98
let snippet = if c.len() > 200 {
format!("{}...", &c[..200]) // 🔴 panics if byte 200 is mid-charBoth paths are exercised with Chinese/Japanese input (the exact scenario that caused the original crash). The The fix: extract // rag.rs
let snippet = crate::utils::str::truncate_chars(content, 300);
// query_rewriter.rs
let snippet = crate::utils::str::truncate_chars(c, 200);🟡 Medium:
|
| 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>
&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>
|
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 panicTasks:
Status Since Last ReviewGood progress: both critical UTF-8 fixes from the previous two review rounds have been applied. The remaining items are medium/minor. ✅ Critical Issues — Resolved
let snippet = crate::utils::str::truncate_chars(content, 300);
let snippet = crate::utils::str::truncate_chars(c, 200);Both sites now use the shared 🟡 Medium:
|
| 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.
|
@copilot review this changes |
…ng Rust primitive; fix rag test assertion to use chars().count(); add zero-max edge case test Co-authored-by: chinkan <16433287+chinkan@users.noreply.github.com> Agent-Logs-Url: https://github.com/chinkan/RustFox/sessions/df38c695-4336-437e-8324-5b6db9d2c378
|
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 panicTasks:
Status Since Last ReviewAll critical UTF-8 truncation issues are now resolved. The utility was also correctly renamed from ✅ All Critical Issues — Resolved
let snippet = crate::utils::strings::truncate_chars(content, 300);
let snippet = crate::utils::strings::truncate_chars(c, 200);
🟡 Medium:
|
| 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 assertions in truncation tests | Not fixed (new instance found in query_rewriter.rs) |
| 🟢 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 blocking critical issues are all resolved. The remaining items are medium/minor — no safety or correctness blockers in the same class as the UTF-8 panic. The PR is in good shape to merge once the medium items are addressed.
There was a problem hiding this comment.
Pull request overview
This PR addresses a UTF-8 slicing panic by introducing character-safe truncation, but it also significantly expands the bot’s functionality with Telegram streaming, verbose tool UI, RAG + query rewriting, and nightly conversation summarization.
Changes:
- Add UTF-8-safe
truncate_chars()and use it for tool-argument previews and other snippet truncation paths. - Add Telegram UX features:
/verbosetool call progress UI + streamed response delivery via message edits. - Add memory enhancements: conversation-scoped RAG retrieval, optional query rewriting, and nightly summarization (incl. DB migration + scheduler cron wiring).
Reviewed changes
Copilot reviewed 22 out of 23 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/strings.rs | Adds truncate_chars() helper + unit tests. |
| src/utils/mod.rs | Exposes utils::strings. |
| src/platform/tool_notifier.rs | Implements tool notifier + safe args preview formatting + tests. |
| src/platform/telegram.rs | Adds /verbose, tool notifier wiring, and streaming message-edit loop + tests. |
| src/platform/mod.rs | Exposes tool_notifier module. |
| src/agent.rs | Updates process_message signature; injects RAG context; emits tool events; adds token streaming. |
| src/memory/rag.rs | Adds auto-retrieval with optional query rewriting; truncates snippets safely + tests. |
| src/memory/query_rewriter.rs | Adds follow-up query rewriting logic + tests. |
| src/memory/summarizer.rs | Adds nightly summarization logic + tests for store helpers/guards. |
| src/memory/conversations.rs | Adds message limiting, conversation-scoped search, summarization helpers + tests. |
| src/memory/mod.rs | Registers new memory modules and adds is_summarized column migration. |
| src/scheduler/tasks.rs | Registers nightly summarization cron job. |
| src/main.rs | Wires new process_message signature and summarization task registration/config. |
| src/llm.rs | Adds OpenRouter SSE parsing + chat_stream() + tests. |
| src/config.rs | Adds memory config fields (RAG limit, raw cap, summarization schedule/threshold) + prompt updates. |
| README.md | Documents newly added streaming/RAG/summarization/verbose features. |
| Cargo.toml | Enables reqwest streaming + adds futures-util dependency. |
| Cargo.lock | Lockfile updates for new dependency usage. |
| docs/plans/* | Adds/updates detailed design and implementation plan documents. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| fn test_format_args_preview_multi_arg_chinese_panics_before_fix() { | ||
| // Multi-arg JSON falls through to the raw-JSON fallback path (lines 43-44). | ||
| // This test currently PANICS (fails) because &args_json[..60] hits byte 60 | ||
| // inside the multi-byte character '香'. After the UTF-8 truncation fix is | ||
| // applied, the slice will be adjusted to a safe boundary and this test will pass. |
There was a problem hiding this comment.
This test name and comment still describe the pre-fix behavior ("panics before fix"), but the implementation now uses UTF-8-safe truncation and should not panic. Please rename the test and update the comment to reflect the current invariant being tested (regression: does not panic / returns valid UTF-8).
| fn test_format_args_preview_multi_arg_chinese_panics_before_fix() { | |
| // Multi-arg JSON falls through to the raw-JSON fallback path (lines 43-44). | |
| // This test currently PANICS (fails) because &args_json[..60] hits byte 60 | |
| // inside the multi-byte character '香'. After the UTF-8 truncation fix is | |
| // applied, the slice will be adjusted to a safe boundary and this test will pass. | |
| fn test_format_args_preview_multi_arg_chinese_utf8_safe() { | |
| // Multi-arg JSON falls through to the raw-JSON fallback path. | |
| // Regression test: ensure that truncation is UTF-8 safe, does not panic, | |
| // and returns a non-empty, valid UTF-8 preview for Chinese text. |
| pub async fn register_builtin_tasks( | ||
| scheduler: &Scheduler, | ||
| _memory: MemoryStore, | ||
| llm: crate::llm::LlmClient, | ||
| summarize_cron: String, | ||
| summarize_threshold: usize, |
There was a problem hiding this comment.
register_builtin_tasks now uses the MemoryStore argument, so the leading underscore in _memory is misleading and makes it easy to miss that it’s part of the task wiring. Consider renaming _memory to memory (and updating the local clones accordingly).
| # HTTP client for OpenRouter | ||
| reqwest = { version = "0.12", features = ["json"] } | ||
| reqwest = { version = "0.12", features = ["json", "stream"] } | ||
|
|
There was a problem hiding this comment.
The PR title/description indicate a narrow UTF-8 truncation crash fix in tool_notifier, but this PR also introduces major new capabilities (OpenRouter streaming deps, RAG/query rewriting, nightly summarization, Telegram streaming UI, etc.). Please either update the PR metadata to match the actual scope or split these features into separate PRs to make review/risk management tractable.
| /// Truncates `s` to at most `max_chars` Unicode scalar values. | ||
| /// Appends "..." if truncation occurred. | ||
| /// Safe for any UTF-8 input including Chinese, Japanese, emoji, etc. | ||
| pub 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]); | ||
| } |
There was a problem hiding this comment.
The docstring says the result is truncated to at most max_chars Unicode scalar values, but when truncation occurs the function returns max_chars chars plus the 3-char ellipsis (and for max_chars=0 it returns "..."). Please either adjust the wording to reflect this, or change the implementation so the total output length (including ellipsis) is bounded by max_chars.
| let mut stmt = conn.prepare( | ||
| "SELECT id, role, content FROM messages | ||
| WHERE conversation_id = ?1 | ||
| AND (is_summarized IS NULL OR is_summarized = 0) |
There was a problem hiding this comment.
get_unsummarized_messages currently returns all roles (including system / tool) and will also return existing [SUMMARY] system messages since they default to is_summarized = 0. This means nightly summarization can end up summarizing system prompts and prior summaries ("summary of summary"). Consider filtering to role IN ('user','assistant') (and/or explicitly excluding content LIKE '[SUMMARY]%') so the summarizer only processes conversational turns.
| AND (is_summarized IS NULL OR is_summarized = 0) | |
| AND (is_summarized IS NULL OR is_summarized = 0) | |
| AND role IN ('user', 'assistant') | |
| AND (content IS NULL OR content NOT LIKE '[SUMMARY]%') |
| for id in message_ids { | ||
| conn.execute( | ||
| "UPDATE messages SET is_summarized = 1 WHERE id = ?1", | ||
| rusqlite::params![id], | ||
| ) | ||
| .context("Failed to mark message as summarized")?; | ||
| } |
There was a problem hiding this comment.
mark_messages_summarized runs one UPDATE per message ID. For larger thresholds this can be slow and holds the DB lock longer than necessary. Consider wrapping updates in a transaction and/or doing a single UPDATE ... WHERE id IN (...) to reduce write amplification.
| for id in message_ids { | |
| conn.execute( | |
| "UPDATE messages SET is_summarized = 1 WHERE id = ?1", | |
| rusqlite::params![id], | |
| ) | |
| .context("Failed to mark message as summarized")?; | |
| } | |
| // Build a single UPDATE statement that marks all given message IDs as summarized. | |
| let placeholders = std::iter::repeat("?") | |
| .take(message_ids.len()) | |
| .collect::<Vec<_>>() | |
| .join(","); | |
| let sql = format!( | |
| "UPDATE messages SET is_summarized = 1 WHERE id IN ({})", | |
| placeholders | |
| ); | |
| conn.execute( | |
| &sql, | |
| rusqlite::params_from_iter(message_ids.iter()), | |
| ) | |
| .context("Failed to mark message as summarized")?; |
| // Notify tool completion | ||
| if let Some(ref tx) = tool_event_tx { | ||
| let success = !tool_result.starts_with("Error"); | ||
| let _ = | ||
| tx.try_send(crate::platform::tool_notifier::ToolEvent::Completed { | ||
| name: tool_call.function.name.clone(), | ||
| success, | ||
| }); |
There was a problem hiding this comment.
Tool success is inferred via !tool_result.starts_with("Error"), but execute_tool returns many failure strings that don't start with "Error" (e.g. "Failed to ...", "Missing ...", "Invalid ..."), so failures will be reported as success in the verbose UI. Prefer returning a structured result (e.g. Result<String> from execute_tool) or otherwise propagating an explicit success flag alongside the message.
Summary
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)truncate_chars()helper that walks.chars()to find the safe byte boundary before slicingRoot Cause
String::len()returns byte length, not char count. Theschedule_taskargs 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 deadcascade crash.Test Plan
test_format_args_preview_multi_arg_chinese_panics_before_fix— reproduces the exact production crash input, now passes after fixtest_format_args_preview_single_arg_with_chinese— exercises single-arg path with Chinese stringcargo test)cargo clippy -- -D warningscleancargo fmt --checkclean🤖 Generated with Claude Code