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.
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
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.
Adds OpenAI-compatible image_url content blocks to the chat message
model, wiring attached images through build_chat_messages_with_reasoning
as multimodal user-content arrays. When images are present, user
messages emit a content array of text + image_url parts instead of a
plain string, matching the OpenAI vision API shape.
- models.rs: new ImageUrlContent struct, ContentBlock::ImageUrl variant
- client/chat.rs: image_parts collection, multimodal wire format for
user messages, image-aware message inspection, stream-event no-op
- Exhaustiveness arms added across 10 files (compaction, seam_manager,
capacity_flow, purge, notifications, session_picker, utils,
working_set, rlm/session, runtime_api)
- Test: request_builder_emits_openai_image_url_parts_for_user_images
Credit: @xyuai (PR #2587 — root cause + initial implementation)
Closes: #2584
Co-authored-by: xyuai <xyuai@users.noreply.github.com>
Adds 8 missing Fixed entries for commits that landed after the
release-prep commit (06612495f): DEC CSI fragment fix, engine panic
recovery, nested file-picker, command-palette scroll, .NET/Windows
env, config key warnings, diff-render whitespace, and model
persistence. Adds Community credits for contributors whose work
landed or shaped this release cycle.
`workspace_completions_honor_configured_walk_depth` placed its probe file at
component depth 9 and asserted the *default* walk excludes it — true at the
old default (6) but not the new one (10). Move the probe to depth 12 so it
stays past the default while remaining within the explicit deeper walk (16)
and the unlimited (0) cases the test also exercises.
https://claude.ai/code/session_01MQrnh6wHfrEYN5BBdMarC1
`legacy_sse_closed_stream_reconnects_and_retries_tool_call` passed in
isolation but flaked under parallel load. The mock server dropped the
tool-call response whenever `active_sse` was momentarily `None` — which
happens when the retry POST is scheduled ahead of the reconnecting GET /sse
that re-stores the SSE sender. The client then hung until timeout and the
test failed.
Make the server wait briefly (bounded, 5s) for the SSE channel before
sending, so response delivery no longer depends on the order in which the
two server tasks are scheduled.
https://claude.ai/code/session_01MQrnh6wHfrEYN5BBdMarC1
Resolves the workspace clippy warnings so the release gate
(`cargo clippy --workspace --all-targets -- -D warnings`) is clean:
- chat.rs: elide needless lifetime on `next_arcee_waf_trigger` (returned
strs are `'static`).
- llm_client/mod.rs: use `enumerate()` instead of a manual loop counter in
`truncate_for_error` (explicit_counter_loop).
- ui.rs: `#[allow(clippy::too_many_arguments)]` on `apply_model_picker_choice`
with rationale (8 distinct handles/states; a struct would only obscure it).
- file_picker.rs: gate the test-only `WALK_DEPTH` const and `new_with_relevance`
convenience ctor behind `#[cfg(test)]` (the #2488 change moved production
callers to `new_with_relevance_and_depth`).
The 6 schema_migration registry structs the issue noted are no longer flagged
(their trait impls keep them live). Also normalizes rustfmt formatting on the
touched lines.
https://claude.ai/code/session_01MQrnh6wHfrEYN5BBdMarC1
Shell tools (`exec_shell`, `task_shell_start`, …) only register when
`allow_shell` is true, but `allow_shell` and `sandbox_mode` are top-level
keys. Placing them under a `[general]` or `[sandbox]` table — neither of
which CodeWhale defines — makes TOML silently drop them, so `allow_shell`
stays false and the tools vanish from the catalog with no explanation.
Following the existing `warn_on_misplaced_root_base_url` precedent, emit a
startup warning naming the misplaced keys and telling the user to move them
to the top of the file. With the keys correctly placed, shell tools register
on Windows too (no sandbox required for danger-full-access).
https://claude.ai/code/session_01MQrnh6wHfrEYN5BBdMarC1
`exec_shell` runs with `env_clear()` plus a strict allowlist. On Windows
there is no sandbox, so commands run directly — but the allowlist dropped
`APPDATA`, `LOCALAPPDATA`, `ProgramData`, `ProgramFiles*`, and the `DOTNET_*`
/ `NUGET_*` variables that `dotnet restore` and NuGet rely on to locate
their package cache, HTTP cache, and config. Restore therefore failed
through the tool while working in the user's own shell, where the full
environment is present.
Add the .NET/NuGet and Windows app-data path variables to the shell
allowlist (`DOTNET_*` via prefix, like `LC_*`). NuGet credential vars
(`NuGetPackageSourceCredentials_*`) still fall outside the allowlist and are
not exported. Also benefits npm/pip on Windows, which use the same paths.
https://claude.ai/code/session_01MQrnh6wHfrEYN5BBdMarC1
The palette sized its scroll window by entry count (`popup_height - 7`)
while the same fixed-height popup also rendered the 9-line header plus
per-section labels and separators. Those uncounted rows pushed the selected
entry past the bottom clip line, so pressing Down made the cursor vanish and
the list appear frozen until the index finally exceeded the oversized budget.
Size the window against the real rendered cost: subtract the actual header
height and account for section labels/separators when choosing the visible
range, guaranteeing the selection stays on screen and the list scrolls.
Adds unit tests for the window helper.
https://claude.ai/code/session_01MQrnh6wHfrEYN5BBdMarC1
`ignore`'s `max_depth(Some(6))` excludes files inside a 6-level-deep
directory (they sit at component depth 7), so @-mention completion and the
Ctrl+P picker silently missed them. Raise the default walk depth to 10
(covers conventionally nested Java/.NET/web trees) and make the Ctrl+P
picker honor the configurable `mention_walk_depth` — including `0` for an
unlimited walk — so it matches @-mention behavior and the existing
"set mention_walk_depth 0 to search deeper" guidance.
The walk stays bounded by `.gitignore` and `MAX_CANDIDATES`. Adds a
regression test covering depth-6 miss, default reach, and unlimited.
https://claude.ai/code/session_01MQrnh6wHfrEYN5BBdMarC1
A panic inside `handle_deepseek_turn` unwound through `engine.run()` and was
caught by `spawn_supervised("engine-event-loop")`, which wrote a crash dump
and let the whole engine task exit. The UI never received `TurnComplete`, so
it sat on "working" forever and every subsequent turn was dead too — exactly
the "the engine have stopped" / stuck-on-working reports.
Wrap the turn call in `catch_unwind` so a panic now surfaces as a failed
`TurnComplete` (with a clear, actionable message) and the engine keeps
running. The crash dump is still written via a new `record_caught_panic`
helper so maintainers retain the `~/.codewhale/crashes/` diagnostics.
Also dedupes the panic-message extraction in `spawn_supervised` /
`spawn_blocking_supervised` into a shared `panic_message` helper.
https://claude.ai/code/session_01MQrnh6wHfrEYN5BBdMarC1
Terminal mode set/reset chatter (bracketed paste `[?2004h/l`, mouse
capture `[?1000h`, focus reporting `[?1004h`, synchronized output
`[?2026h`) ends in `h`/`l`, but the composer's CSI fragment filter only
treated `u` (Kitty keyboard) as a terminator. During dense streaming the
leading `[` leaked into the input and corrupted editing — a regression of
the #1915 control-sequence filter.
Accept `h`/`l` as terminators too, but only after a numeric parameter so
ordinary prose like `[?help]` is preserved (real mode sequences are always
`[?<number><letter>`; Kitty's parameter-less `[?u` still matches). Adds
regression tests for the mode sequences and the false-positive guard.
https://claude.ai/code/session_01MQrnh6wHfrEYN5BBdMarC1
Full changelog in CHANGELOG.md. macOS failure is pre-existing flaky MCP network test; Windows was a CI timeout during compilation. Both unrelated to harvested changes.
Users copying text from the output area and right-clicking in the
composer expect Paste to be the first, most accessible action.
Previously Paste appeared after all cell-specific actions (Open
Details, Copy Message, etc.), requiring extra mouse travel or
keyboard navigation.
Reported by a WeChat/Chinese UX user during the v0.8.50 triage pass.