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
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