Skip to content

[BUG] Reasoning text rendered twice in Code mode (stale partial snapshot not merged) #1346

Description

@ShaderFallback

Describe the bug

In Code mode, the model's reasoning/thinking text is sometimes rendered twice in the chat panel. The model only infers once — a single API request containing a single reasoning block (verified in api_conversation_history.json, token usage is normal). The duplication is intermittent and always consists of the same reasoning text appearing as a pair: a frozen streaming snapshot followed by the final version, identical except for 1–2 trailing characters (e.g. a missing trailing period).

This is a UI message-store defect, not a double inference and not a provider parsing issue.

To Reproduce

Steps to reproduce the behavior:

Go to a new task in Code mode with an OpenAI-compatible thinking model configured (e.g. GLM-4.7, Qwen3-thinking, DeepSeek-R1)
Click send on a prompt that triggers multiple tool calls, e.g. "List all .ps1 files on the desktop and inspect each one"
Wait for the model to emit a reasoning segment and then invoke a tool (reasoning block followed by a tool_use block in the same response)
See the thinking area render the same reasoning text twice (frozen streaming snapshot + final write)
Deterministic verification (no visual guesswork):

Open the task's message store:

%APPDATA%\Code\User\globalStorage\zoocodeorganization.zoo-code\tasks<task-id>\ui_messages.json

The bug is confirmed by an adjacent pair like:

{"type":"say","say":"reasoning","partial":true, "text":"...(identical for the first N chars)"}
{"type":"say","say":"reasoning","partial":false, "text":"...(1-2 extra trailing chars)"}

With a correct merge, only the single partial:false entry should exist.

Expected behavior

The reasoning text is rendered once. The final partial:false write should merge into (replace) the in-flight partial:true snapshot of the same message instead of appending a new entry.

Screenshots

N/A — the bug is data-observable; the ui_messages.json pair above is more precise than a screenshot. (Happy to provide one on request.)

Video

N/A — see verification steps above.

What version of zoo are you running

3.79.100401 (Windows 10, VS Code)

Additional context

Root cause (traced in the bundled dist/extension.js):

Streaming reasoning calls say("reasoning", text, undefined, /partial/ true) on every delta. The dedup logic in say() only inspects the last message:

let d = this.clineMessages.at(-1);
let f = d && d.partial && d.type === "say" && d.say === e;

If any other message (ask / tool / api_req, …) is appended while the reasoning stream is in flight, the final partial:false write no longer sees a same-type partial snapshot as the last message, so it takes the append branch and creates a second entry. The orphaned partial:true snapshot is never removed → both are persisted and both are rendered.

Because it depends on the interleaving of message appends, the bug is a timing race: the same version intermittently duplicates (more frequent with many tool calls, short reasoning segments, and larger stream delta intervals).

Scale of observed data: 655 such duplicate snapshot pairs across 34 historical task directories on this machine.

Distinction from existing issues:

#846 (Ollama dual-channel reasoning: message.thinking + think tags) — different mechanism (provider emits reasoning twice). Here the provider emits it once; the duplication is created entirely client-side in the message store.
#506 (OpenAI Native dual SSE handlers) — different layer (event processing), not message persistence.
Related upstream report with identical symptoms: Kilo-Org/kilocode#5294 (closed as not planned).
Verified fix (local patch, available as a PR):

In both "append new message" branches of say(), scan clineMessages backwards for an existing message with partial === true, same type/say, and prefix-related text (newText.startsWith(oldText) || oldText.startsWith(newText)); if found, merge in place (update text/images/partial flags, keep original ts) instead of appending. Patch applied to 3.79.100401: zero new duplicates since, 655 historical duplicates cleaned. The full patch code and before/after diff are in my writeup — I can submit this as a PR if that is easier than an issue here.

extension.js.patched.zip

extension.js.patched.zip

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions