Skip to content

fix: critical bugs in tools, client, and commands (9 bugs)#2880

Merged
Hmbown merged 4 commits into
Hmbown:mainfrom
HUQIANTAO:fix/critical-bugs-batch-1
Jun 10, 2026
Merged

fix: critical bugs in tools, client, and commands (9 bugs)#2880
Hmbown merged 4 commits into
Hmbown:mainfrom
HUQIANTAO:fix/critical-bugs-batch-1

Conversation

@HUQIANTAO

@HUQIANTAO HUQIANTAO commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes 9 critical bugs across the TUI tools, LLM client, and command system that could cause panics, data corruption, or incorrect behavior.

Bugs Fixed

Panics / Crashes

  1. UTF-8 boundary panic in clean_pdf_text (tools/file.rs:295)

    • str::rfind returns the byte index of the start of the matching character. Using i + 1 may produce an offset that is not a valid UTF-8 char boundary, causing a runtime panic on &str slicing.
    • Fix: Use i + char.len_utf8() to compute the correct byte offset.
  2. ContentBlockStop wrong index on SSE stream cleanup (client/chat.rs:435)

    • When the SSE stream breaks abnormally, the cleanup code used content_index.saturating_sub(1) which would produce u32::MAX when content_index is 0 (first block). The index should be content_index directly since it is only incremented AFTER a block is closed.
    • Fix: Use content_index directly for the close event.
  3. TOCTOU double-call in run_skill_by_name (commands/mod.rs:671)

    • Pattern is_some() followed by unwrap() calls the function twice. If state changes between calls, the second call may return None and panic.
    • Fix: Replace with if let Some(result) = ... pattern.
  4. Silent token count truncation (client.rs:1298)

    • u64 to u32 cast via as silently truncates values above 4,294,967,295. This is inconsistent with the sanitizer at line 1807 which uses .min(u32::MAX).
    • Fix: Use .min(u64::from(u32::MAX)) as u32 for saturating conversion.

Data Integrity

  1. Incomplete hex decoding in from_api_tool_name (client.rs:54-76)

    • The decoder accepts fewer than 6 hex digits, while the encoder always produces exactly 6. This inconsistency means a mangled tool name with a short hex sequence would be decoded differently by the two code paths.
    • Fix: Require exactly 6 hex digits to match encoder output.
  2. HTTP response body read errors silently swallowed (client/chat.rs:170, client.rs:750)

    • unwrap_or_default() converts connection drops mid-body to empty string "", causing an unhelpful JSON parse error instead of surfacing the real cause.
    • Fix: Replace with .context()? error propagation.
  3. Integer overflow in context_lines (tools/search.rs:103)

    • Model-provided context_lines has no upper bound. On 32-bit platforms, usize::MAX fallback from try_from combined with addition causes overflow.
    • Fix: Clamp to .min(1000) to prevent excessive memory allocation.
  4. u64 to usize truncating cast (tools/file.rs:117,127)

    • as usize silently truncates on 32-bit platforms. A model passing start_line = 5_000_000_000 would read from the wrong line.
    • Fix: Use usize::try_from() with proper error propagation.
  5. Missing providers in provider_accepts_reasoning_content (client/chat.rs:1966)

    • SiliconflowCn and Volcengine are present in apply_reasoning_effort but missing from provider_accepts_reasoning_content. Non-DeepSeek reasoning models on these providers lose thinking trace support.
    • Fix: Add both providers to the match list.

Hu Qiantao and others added 2 commits June 3, 2026 20:23
Add prompt_persist.rs module that caches the immutable base section of
the system prompt on disk for cross-session reuse. The base section
(mode prompt, project context, skills, context management, compaction
template) is stable across sessions for the same workspace. By caching
this section and reusing it when the SHA-256 matches, we can skip the
entire base-section assembly on session start and immediately provide
byte-identical bytes to the API.

This is especially valuable for DeepSeek's service-side prefix cache:
when the base section bytes are identical across sessions, the server
can reuse its cached KV states for the entire base section, giving ~90%
discount on cached tokens.

Cache layout:
  ~/.codewhale/prompt_cache/<system_hash>.bin   — the base section text
  ~/.codewhale/prompt_cache/<system_hash>.meta  — JSON metadata

The cache key is the SHA-256 of the base section text. The metadata
includes the workspace path and its mtime for invalidation on workspace
changes. Stale entries are evicted lazily based on age and workspace
mtime consistency.

The module exposes three public functions:
- load_cached_base_section(): try to load a cached base section
- save_cached_base_section(): save a base section to disk
- evict_stale_entries(): clean up old cache entries

This is the infrastructure layer only. Wiring it into the prompt
assembly pipeline (splitting base_section() + volatile_section()) will
be done in a follow-up change.
1. Fix UTF-8 boundary panic in clean_pdf_text (tools/file.rs:295)
   - rfind returns byte index of char start, i+1 may not be char boundary
   - Use char-aware byte offset calculation instead

2. Fix integer overflow in context_lines (tools/search.rs:103)
   - Clamp model-provided context_lines to 1000 to prevent massive allocations
   - On 32-bit, usize::try_from(u64::MAX) falls back to usize::MAX causing overflow

3. Fix u64->usize truncating cast (tools/file.rs:117,127)
   - Use usize::try_from() with proper error instead of silent truncation
   - Prevents reading from wrong line on 32-bit platforms

4. Fix ContentBlockStop wrong index in SSE stream cleanup (client/chat.rs:435)
   - saturating_sub(1) on 0u32 wraps to u32::MAX when stream breaks during thinking
   - Merge thinking/text close into single guard to avoid duplicate stops

5. Fix missing providers in provider_accepts_reasoning_content (client/chat.rs:1966)
   - Add SiliconflowCn and Volcengine which are in apply_reasoning_effort
   - Without this, non-DeepSeek reasoning models on these providers lose thinking traces

6. Fix TOCTOU double-call in run_skill_by_name (commands/mod.rs:671)
   - Replace is_some()+unwrap() with if-let-Some pattern
   - Prevents potential panic if state changes between calls

7. Fix incomplete hex decoding in from_api_tool_name (client.rs:54-76)
   - Require exactly 6 hex digits to match encoder output
   - Short sequences from malformed model output now pass through as-is

8. Fix token count u64->u32 truncation (client.rs:1298-1299)
   - Use .min(u32::MAX) saturating cast consistent with sanitizer at line 1807
   - Prevents silent wraparound for extremely large token counts

9. Fix HTTP response body read errors silently swallowed (client/chat.rs:170, client.rs:750)
   - Replace unwrap_or_default() with .context()? propagation
   - Connection drops mid-body now surface as clear error instead of JSON parse failure

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HUQIANTAO has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown

Thanks @HUQIANTAO for taking the time to contribute.

This repository is currently observing a maintainer-managed contribution gate in dry-run mode, so this pull request is staying open. When enforcement is enabled, pull requests from contributors who are not listed in .github/APPROVED_CONTRIBUTORS will be closed automatically.

Please read CONTRIBUTING.md for the expected contribution shape. A maintainer can grant PR access by commenting /lgtm on a pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces cross-session persistence for the system prompt's base section to optimize DeepSeek's KV prefix cache, along with various bug fixes, safety improvements (such as safe integer conversions and UTF-8 boundary handling), and API provider updates. The code review highlights two main issues: a logical error in crates/tui/src/client/chat.rs where using content_index.saturating_sub(1) targets the wrong block index when closing open stream blocks, and a design flaw in crates/tui/src/prompt_persist.rs where using the workspace directory's mtime for cache invalidation leads to unnecessary cache misses.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread crates/tui/src/client/chat.rs Outdated
Comment on lines 437 to 444
// Close any open blocks — use the current content_index
// (which points to the next unused slot, so -1 gives the
// last-opened block) but guard against underflow when no
// content block was ever opened.
if thinking_started || text_started {
let idx = content_index.saturating_sub(1);
yield Ok(StreamEvent::ContentBlockStop { index: idx });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

这里存在一个严重的逻辑错误。在 parse_sse_chunk 中,当 ThinkingText 数据块处于打开状态时,content_index 并没有被递增(它只有在数据块被关闭时才会递增)。因此,当前打开的数据块的实际索引就是 content_index

如果在这里使用 content_index.saturating_sub(1),会导致关闭的是前一个已经关闭的数据块(或者在第一个数据块时关闭索引 0,这只是碰巧正确),而当前真正打开的数据块将永远不会被关闭。这会导致流事件的索引错乱。

应当直接使用 content_index 作为关闭的索引,无需减 1。

            // Close any open blocks — use the current content_index
            // which points to the currently active open block.
            if thinking_started || text_started {
                yield Ok(StreamEvent::ContentBlockStop { index: content_index });
            }

Comment on lines +104 to +112
// Verify workspace mtime hasn't changed (guards against hash collisions).
let current_mtime = dir_mtime_secs(workspace);
if current_mtime != meta.workspace_mtime_secs {
logging::info(format!(
"Prompt cache stale: workspace mtime changed ({meta_mtime} → {current_mtime})",
meta_mtime = meta.workspace_mtime_secs
));
return None;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

使用工作区目录的 mtime 作为缓存失效的校验存在设计缺陷。在实际开发中,工作区根目录下的任何文件创建、删除或重命名(例如 Git 操作、临时文件生成等)都会改变目录的 mtime,这会导致不必要的缓存失效(Cache Miss),从而降低缓存命中率。由于 SHA-256 碰撞的概率极低,这种针对哈希碰撞的防御性设计不仅没有必要,反而会因为频繁的 mtime 变化而严重影响缓存的性能和效率。建议移除 mtime 校验,仅依赖 base_hash 和工作区路径进行校验。

The content_index is only incremented AFTER a block is closed, not
when opened. Using saturating_sub(1) would close the wrong block.
The reviewer correctly identified this logic error.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HUQIANTAO has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HUQIANTAO has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@HUQIANTAO HUQIANTAO force-pushed the fix/critical-bugs-batch-1 branch from 5121cb6 to 186b5b4 Compare June 7, 2026 11:44

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HUQIANTAO has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

Hmbown added a commit that referenced this pull request Jun 7, 2026
Harvests 7 safe fixes from PR #2880 by @HUQIANTAO: tool-name hex-digit
guard, token-usage u32 clamp, read-file line usize::try_from, grep
context-lines cap, UTF-8 PDF trim, run_skill dedup, and
Volcengine/SiliconflowCn reasoning_content support. Excludes the
DeepSeek stream-stop change and the unwired prompt_persist module
(deferred for separate review).

Co-Authored-By: HUQIANTAO <58421104+HUQIANTAO@users.noreply.github.com>
@Hmbown Hmbown merged commit fb341a9 into Hmbown:main Jun 10, 2026
9 checks passed
@Hmbown

Hmbown commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Merged — thank you @HUQIANTAO. All nine fixes reviewed individually; each is a real bug with the right minimal fix. The ContentBlockStop index correction and the strict 6-hex-digit decode are particularly good catches.

One note: crates/tui/src/prompt_persist.rs landed as a fully #[allow(dead_code)] module that nothing calls yet, and it wasn't mentioned in the PR description. It's inert so it didn't block the merge, but please follow up: either a PR that actually wires it into prompt assembly (coordinating with the prefix-cache work in #2264/#2949 so the designs don't collide), or a removal. Unreferenced scaffolding shouldn't accumulate on main.

@Hmbown Hmbown added this to the v0.8.56 milestone Jun 10, 2026
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