toModelOutput: ({output}) => ({type:"text", value: formatSearchForModel(output)})
function formatSearchForModel(output) {
if (output.matchedNamespaces.length === 0) ... // no optional chaining / no shape guard
Two defects compound:
Writer: tool results can be mis-attributed across parts under parallel execution (a start_process output stored on a tool_search part; companion tool_invoke parts show state: "output-error", output: null).
Reader: formatSearchForModel dereferences output.matchedNamespaces.length without a guard, so any foreign shape crashes the entire message conversion.
Evidence (DB forensics, topic 5ddd06b0-…, message 01a00b03-…)
part #52: type=tool-tool_search, state=output-available, output={'content':[{'type':'text','text':'Process started with PID 26095 …'}], 'metadata':…} (a desktop-commander result, not {matchedNamespaces:[…]}).
All other tool_search parts in the same message have output={'matchedNamespaces':[…]} (correct shape).
Suggested fix
Reader: const ns = output?.matchedNamespaces; if (!Array.isArray(ns)) return "No tools matched…";
Writer: validate/serialize tool outputs per tool schema; never attach one tool's result to another tool's part; keep output-error parts out of toModelOutput's happy path.
Workaround applied locally
SQLite patch normalizing the malformed part to {"matchedNamespaces": []} (backup kept). Topic recovered after restart.
TEXT
---
## Issue 2 · Responses API 序列化配对断裂(400)
```markdown
**Title:** [Bug] v2.0.5 DeepSeek Responses API rejects migrated topics: 400 "No tool output found for tool call …" (function_call / function_call_output pairing broken in serialization)
**Summary**
For a topic created under v1.9.x and continued under v2.0.5, the serialized `input` array sent to `https://api.deepseek.com/v1/responses` interleaves an assistant text message *between* a batch of `function_call` items and their `function_call_output` items. DeepSeek's Responses-compatible endpoint rejects the request with 400 `invalid_request_error: No tool output found for tool call <id>`.
**Steps to reproduce**
1. Under v1.9.x, have a turn with multiple parallel tool calls (e.g. 3× list-tools) followed by assistant text and the tool outputs.
2. Upgrade to v2.0.5 and continue the same topic with a DeepSeek model routed through `/v1/responses`.
3. Send any message.
**Expected behavior**
History is re-serialized into a spec-valid item order (each `function_call` paired with its `function_call_output` without an intervening assistant message), or the provider falls back to chat-completions.
**Actual behavior**
400 from the provider; `AI_APICallError` logged; topic unusable.
**Evidence (app-error log, 2026-08-15 18:16:04, topic d0c12848-…)**
`requestBodyValues.input` order: `[developer, user, assistant(text), function_call×3, assistant(text), function_call_output×3, …]` — the three outputs exist but are separated from their calls by an assistant message, which the endpoint does not accept.
**Suggested fix**
- In the Responses-API serializer, emit `function_call_output` items immediately after their `function_call` group, before any subsequent assistant message; add a migration pass for v1-era topics; validate pairing before POST (dev assertion).
**Workaround**
Continue in a new topic (clean serialization), or delete the first parallel-tool round from the old topic.
Issue 3 · 特殊 token 预检形成自增强错误环
**Title:** [Bug] v2.0.5 "Disallowed special token found" pre-flight creates a permanent self-reinforcing error loop (error text re-triggers the check on every subsequent send)
**Summary**
`prepareChatMessages` counts tokens with the o200k_base encoding and throws if the message text contains ChatML special-token literals (the sequences `<|`+`im_start`+`|>`, `<|`+`im_end`+`|>`, `<|`+`endoftext`+`|>`, `<|`+`endofturn`+`|>`, `<|`+`endofmessage`+`|>`). Once thrown, the error text itself (which embeds the literal) is persisted as a `data-error` part in history. Every later send re-assembles that history, the pre-flight hits the literal again, throws again, and persists again — a permanent loop that no new user message can escape.
**Steps to reproduce**
1. Any event causes the literal to appear in a request (user quote, tool output, or a prior error part).
2. Pre-flight throws "Disallowed special token found: …".
3. Cherry Studio stores the error (including the literal) as a `data-error` part.
4. Send any new message → same error, forever.
**Expected behavior**
- Sanitize/escape before counting instead of throwing, or exclude `data-error` parts from the pre-flight, or make the check configurable.
- Never persist error text that re-triggers the same check.
**Actual behavior**
Error: Disallowed special token found: <|im_start|> (shown with full delimiters in UI)
at GptEncoding.countTokens (o200k_base-*.js:205663)
at Object.count (main.js:85290)
at allocateInlineCaps (main.js:86009) / applyInlineCaps (86487) / prepareChatMessages (86471)
at AiService$1.streamText (main.js:140614)
TEXT
Note: code blocks / backticks do NOT exempt the literal — it is a plain substring scan.
**Evidence (DB forensics)**
- `message` rows 4327–4333: `data` and generated `searchable_text` contained the literals; row 4327's `data` is exactly a `data-error` part whose `message`/`stack` embed the literal — confirming the loop.
- After neutralizing the literals in `data` (searchable_text regenerates automatically), `REMAINING = 0` and the topic recovers on restart.
**Suggested fix**
- Replace throw-with-persist by sanitize-then-count (strip/escape the five literals pre-count).
- If the check stays: skip `data-error` parts, and cap the loop by not writing error parts that contain the trigger substrings.
**Workaround applied locally**
SQLite patch replacing each literal with its delimiter-free name (e.g. `<|`+`im_start`+`|>` → `im_start`), with backup. Idempotent script available on request.
### Steps To Reproduce
公共 Environment 块(三帖共用)
**Environment**
- Cherry Studio: v2.0.5 (linux x64, packaged), upgraded 1.9.12 → 1.9.13 → 2.0.5
- OS: Ubuntu (native, not WSL)
- Store: ~/.config/CherryStudio/Data/cherrystudio.sqlite (SQLite, WAL)
- Provider involved: DeepSeek (deepseek-v4-pro) via https://api.deepseek.com/v1/responses
- MCP servers active: desktop-commander, gc-filesystem, Firecrawl, Playwright, browser-use, crawl4ai, web-access, spiderfoot-bridge (14 child processes, all healthy at incident time)
Issue 1 · tool_search 畸形 output 导致整次发送崩溃
**Title:** [Bug] v2.0.5 TypeError "Cannot read properties of undefined (reading 'length')" in formatSearchForModel — one malformed tool_search part crashes the entire send
**Summary**
A single `tool-tool_search` part whose stored `output` has a wrong shape (raw MCP result of *another* tool) makes `convertToModelMessages` throw, so the whole conversation send fails with a client-side TypeError instead of a graceful error.
**Steps to reproduce**
1. In a topic, run several parallel `tool_search` / `tool_invoke` meta-tool calls (tool-discovery flow).
2. Under parallel execution, one `tool-tool_search` part gets written with the output of an unrelated tool (evidence below: a desktop-commander `start_process` result landed inside a `tool_search` part).
3. Send any next message in that topic.
**Expected behavior**
The send proceeds; at worst the malformed part is rendered with a fallback ("No tools matched…").
**Actual behavior**
TypeError: Cannot read properties of undefined (reading 'length')
at formatSearchForModel (main.js:128337)
at Object.toModelOutput (main.js:128332)
at createToolModelOutput (main.js:46383)
at processBlock (main.js:51549)
at convertToModelMessages (main.js:51570)
TEXT
The topic becomes unusable; every retry stores a `data-error` part and fails again.
**Root cause (from app.asar)**
```js
toModelOutput: ({output}) => ({type:"text", value: formatSearchForModel(output)})
function formatSearchForModel(output) {
if (output.matchedNamespaces.length === 0) ... // no optional chaining / no shape guard
Two defects compound:
Writer: tool results can be mis-attributed across parts under parallel execution (a start_process output stored on a tool_search part; companion tool_invoke parts show state: "output-error", output: null).
Reader: formatSearchForModel dereferences output.matchedNamespaces.length without a guard, so any foreign shape crashes the entire message conversion.
Evidence (DB forensics, topic 5ddd06b0-…, message 01a00b03-…)
part #52: type=tool-tool_search, state=output-available, output={'content':[{'type':'text','text':'Process started with PID 26095 …'}], 'metadata':…} (a desktop-commander result, not {matchedNamespaces:[…]}).
All other tool_search parts in the same message have output={'matchedNamespaces':[…]} (correct shape).
Suggested fix
Reader: const ns = output?.matchedNamespaces; if (!Array.isArray(ns)) return "No tools matched…";
Writer: validate/serialize tool outputs per tool schema; never attach one tool's result to another tool's part; keep output-error parts out of toModelOutput's happy path.
Workaround applied locally
SQLite patch normalizing the malformed part to {"matchedNamespaces": []} (backup kept). Topic recovered after restart.
TEXT
---
## Issue 2 · Responses API 序列化配对断裂(400)
```markdown
**Title:** [Bug] v2.0.5 DeepSeek Responses API rejects migrated topics: 400 "No tool output found for tool call …" (function_call / function_call_output pairing broken in serialization)
**Summary**
For a topic created under v1.9.x and continued under v2.0.5, the serialized `input` array sent to `https://api.deepseek.com/v1/responses` interleaves an assistant text message *between* a batch of `function_call` items and their `function_call_output` items. DeepSeek's Responses-compatible endpoint rejects the request with 400 `invalid_request_error: No tool output found for tool call <id>`.
**Steps to reproduce**
1. Under v1.9.x, have a turn with multiple parallel tool calls (e.g. 3× list-tools) followed by assistant text and the tool outputs.
2. Upgrade to v2.0.5 and continue the same topic with a DeepSeek model routed through `/v1/responses`.
3. Send any message.
**Expected behavior**
History is re-serialized into a spec-valid item order (each `function_call` paired with its `function_call_output` without an intervening assistant message), or the provider falls back to chat-completions.
**Actual behavior**
400 from the provider; `AI_APICallError` logged; topic unusable.
**Evidence (app-error log, 2026-08-15 18:16:04, topic d0c12848-…)**
`requestBodyValues.input` order: `[developer, user, assistant(text), function_call×3, assistant(text), function_call_output×3, …]` — the three outputs exist but are separated from their calls by an assistant message, which the endpoint does not accept.
**Suggested fix**
- In the Responses-API serializer, emit `function_call_output` items immediately after their `function_call` group, before any subsequent assistant message; add a migration pass for v1-era topics; validate pairing before POST (dev assertion).
**Workaround**
Continue in a new topic (clean serialization), or delete the first parallel-tool round from the old topic.
Issue 3 · 特殊 token 预检形成自增强错误环
**Title:** [Bug] v2.0.5 "Disallowed special token found" pre-flight creates a permanent self-reinforcing error loop (error text re-triggers the check on every subsequent send)
**Summary**
`prepareChatMessages` counts tokens with the o200k_base encoding and throws if the message text contains ChatML special-token literals (the sequences `<|`+`im_start`+`|>`, `<|`+`im_end`+`|>`, `<|`+`endoftext`+`|>`, `<|`+`endofturn`+`|>`, `<|`+`endofmessage`+`|>`). Once thrown, the error text itself (which embeds the literal) is persisted as a `data-error` part in history. Every later send re-assembles that history, the pre-flight hits the literal again, throws again, and persists again — a permanent loop that no new user message can escape.
**Steps to reproduce**
1. Any event causes the literal to appear in a request (user quote, tool output, or a prior error part).
2. Pre-flight throws "Disallowed special token found: …".
3. Cherry Studio stores the error (including the literal) as a `data-error` part.
4. Send any new message → same error, forever.
**Expected behavior**
- Sanitize/escape before counting instead of throwing, or exclude `data-error` parts from the pre-flight, or make the check configurable.
- Never persist error text that re-triggers the same check.
**Actual behavior**
Error: Disallowed special token found: <|im_start|> (shown with full delimiters in UI)
at GptEncoding.countTokens (o200k_base-*.js:205663)
at Object.count (main.js:85290)
at allocateInlineCaps (main.js:86009) / applyInlineCaps (86487) / prepareChatMessages (86471)
at AiService$1.streamText (main.js:140614)
TEXT
Note: code blocks / backticks do NOT exempt the literal — it is a plain substring scan.
**Evidence (DB forensics)**
- `message` rows 4327–4333: `data` and generated `searchable_text` contained the literals; row 4327's `data` is exactly a `data-error` part whose `message`/`stack` embed the literal — confirming the loop.
- After neutralizing the literals in `data` (searchable_text regenerates automatically), `REMAINING = 0` and the topic recovers on restart.
**Suggested fix**
- Replace throw-with-persist by sanitize-then-count (strip/escape the five literals pre-count).
- If the check stays: skip `data-error` parts, and cap the loop by not writing error parts that contain the trigger substrings.
**Workaround applied locally**
SQLite patch replacing each literal with its delimiter-free name (e.g. `<|`+`im_start`+`|>` → `im_start`), with backup. Idempotent script available on request.
### Expected Behavior
公共 Environment 块(三帖共用)
**Environment**
- Cherry Studio: v2.0.5 (linux x64, packaged), upgraded 1.9.12 → 1.9.13 → 2.0.5
- OS: Ubuntu (native, not WSL)
- Store: ~/.config/CherryStudio/Data/cherrystudio.sqlite (SQLite, WAL)
- Provider involved: DeepSeek (deepseek-v4-pro) via https://api.deepseek.com/v1/responses
- MCP servers active: desktop-commander, gc-filesystem, Firecrawl, Playwright, browser-use, crawl4ai, web-access, spiderfoot-bridge (14 child processes, all healthy at incident time)
Issue 1 · tool_search 畸形 output 导致整次发送崩溃
**Title:** [Bug] v2.0.5 TypeError "Cannot read properties of undefined (reading 'length')" in formatSearchForModel — one malformed tool_search part crashes the entire send
**Summary**
A single `tool-tool_search` part whose stored `output` has a wrong shape (raw MCP result of *another* tool) makes `convertToModelMessages` throw, so the whole conversation send fails with a client-side TypeError instead of a graceful error.
**Steps to reproduce**
1. In a topic, run several parallel `tool_search` / `tool_invoke` meta-tool calls (tool-discovery flow).
2. Under parallel execution, one `tool-tool_search` part gets written with the output of an unrelated tool (evidence below: a desktop-commander `start_process` result landed inside a `tool_search` part).
3. Send any next message in that topic.
**Expected behavior**
The send proceeds; at worst the malformed part is rendered with a fallback ("No tools matched…").
**Actual behavior**
TypeError: Cannot read properties of undefined (reading 'length')
at formatSearchForModel (main.js:128337)
at Object.toModelOutput (main.js:128332)
at createToolModelOutput (main.js:46383)
at processBlock (main.js:51549)
at convertToModelMessages (main.js:51570)
TEXT
The topic becomes unusable; every retry stores a `data-error` part and fails again.
**Root cause (from app.asar)**
```js
toModelOutput: ({output}) => ({type:"text", value: formatSearchForModel(output)})
function formatSearchForModel(output) {
if (output.matchedNamespaces.length === 0) ... // no optional chaining / no shape guard
Two defects compound:
Writer: tool results can be mis-attributed across parts under parallel execution (a start_process output stored on a tool_search part; companion tool_invoke parts show state: "output-error", output: null).
Reader: formatSearchForModel dereferences output.matchedNamespaces.length without a guard, so any foreign shape crashes the entire message conversion.
Evidence (DB forensics, topic 5ddd06b0-…, message 01a00b03-…)
part #52: type=tool-tool_search, state=output-available, output={'content':[{'type':'text','text':'Process started with PID 26095 …'}], 'metadata':…} (a desktop-commander result, not {matchedNamespaces:[…]}).
All other tool_search parts in the same message have output={'matchedNamespaces':[…]} (correct shape).
Suggested fix
Reader: const ns = output?.matchedNamespaces; if (!Array.isArray(ns)) return "No tools matched…";
Writer: validate/serialize tool outputs per tool schema; never attach one tool's result to another tool's part; keep output-error parts out of toModelOutput's happy path.
Workaround applied locally
SQLite patch normalizing the malformed part to {"matchedNamespaces": []} (backup kept). Topic recovered after restart.
TEXT
---
## Issue 2 · Responses API 序列化配对断裂(400)
```markdown
**Title:** [Bug] v2.0.5 DeepSeek Responses API rejects migrated topics: 400 "No tool output found for tool call …" (function_call / function_call_output pairing broken in serialization)
**Summary**
For a topic created under v1.9.x and continued under v2.0.5, the serialized `input` array sent to `https://api.deepseek.com/v1/responses` interleaves an assistant text message *between* a batch of `function_call` items and their `function_call_output` items. DeepSeek's Responses-compatible endpoint rejects the request with 400 `invalid_request_error: No tool output found for tool call <id>`.
**Steps to reproduce**
1. Under v1.9.x, have a turn with multiple parallel tool calls (e.g. 3× list-tools) followed by assistant text and the tool outputs.
2. Upgrade to v2.0.5 and continue the same topic with a DeepSeek model routed through `/v1/responses`.
3. Send any message.
**Expected behavior**
History is re-serialized into a spec-valid item order (each `function_call` paired with its `function_call_output` without an intervening assistant message), or the provider falls back to chat-completions.
**Actual behavior**
400 from the provider; `AI_APICallError` logged; topic unusable.
**Evidence (app-error log, 2026-08-15 18:16:04, topic d0c12848-…)**
`requestBodyValues.input` order: `[developer, user, assistant(text), function_call×3, assistant(text), function_call_output×3, …]` — the three outputs exist but are separated from their calls by an assistant message, which the endpoint does not accept.
**Suggested fix**
- In the Responses-API serializer, emit `function_call_output` items immediately after their `function_call` group, before any subsequent assistant message; add a migration pass for v1-era topics; validate pairing before POST (dev assertion).
**Workaround**
Continue in a new topic (clean serialization), or delete the first parallel-tool round from the old topic.
Issue 3 · 特殊 token 预检形成自增强错误环
**Title:** [Bug] v2.0.5 "Disallowed special token found" pre-flight creates a permanent self-reinforcing error loop (error text re-triggers the check on every subsequent send)
**Summary**
`prepareChatMessages` counts tokens with the o200k_base encoding and throws if the message text contains ChatML special-token literals (the sequences `<|`+`im_start`+`|>`, `<|`+`im_end`+`|>`, `<|`+`endoftext`+`|>`, `<|`+`endofturn`+`|>`, `<|`+`endofmessage`+`|>`). Once thrown, the error text itself (which embeds the literal) is persisted as a `data-error` part in history. Every later send re-assembles that history, the pre-flight hits the literal again, throws again, and persists again — a permanent loop that no new user message can escape.
**Steps to reproduce**
1. Any event causes the literal to appear in a request (user quote, tool output, or a prior error part).
2. Pre-flight throws "Disallowed special token found: …".
3. Cherry Studio stores the error (including the literal) as a `data-error` part.
4. Send any new message → same error, forever.
**Expected behavior**
- Sanitize/escape before counting instead of throwing, or exclude `data-error` parts from the pre-flight, or make the check configurable.
- Never persist error text that re-triggers the same check.
**Actual behavior**
Error: Disallowed special token found: <|im_start|> (shown with full delimiters in UI)
at GptEncoding.countTokens (o200k_base-*.js:205663)
at Object.count (main.js:85290)
at allocateInlineCaps (main.js:86009) / applyInlineCaps (86487) / prepareChatMessages (86471)
at AiService$1.streamText (main.js:140614)
TEXT
Note: code blocks / backticks do NOT exempt the literal — it is a plain substring scan.
**Evidence (DB forensics)**
- `message` rows 4327–4333: `data` and generated `searchable_text` contained the literals; row 4327's `data` is exactly a `data-error` part whose `message`/`stack` embed the literal — confirming the loop.
- After neutralizing the literals in `data` (searchable_text regenerates automatically), `REMAINING = 0` and the topic recovers on restart.
**Suggested fix**
- Replace throw-with-persist by sanitize-then-count (strip/escape the five literals pre-count).
- If the check stays: skip `data-error` parts, and cap the loop by not writing error parts that contain the trigger substrings.
**Workaround applied locally**
SQLite patch replacing each literal with its delimiter-free name (e.g. `<|`+`im_start`+`|>` → `im_start`), with backup. Idempotent script available on request.
### Relevant Log Output
```shell
Issue Checklist
Platform
Linux
Version
公共 Environment 块(三帖共用)
Bug Description
公共 Environment 块(三帖共用)
Environment
Issue 1 · tool_search 畸形 output 导致整次发送崩溃
Title: [Bug] v2.0.5 TypeError "Cannot read properties of undefined (reading 'length')" in formatSearchForModel — one malformed tool_search part crashes the entire send
Summary
A single
tool-tool_searchpart whose storedoutputhas a wrong shape (raw MCP result of another tool) makesconvertToModelMessagesthrow, so the whole conversation send fails with a client-side TypeError instead of a graceful error.Steps to reproduce
tool_search/tool_invokemeta-tool calls (tool-discovery flow).tool-tool_searchpart gets written with the output of an unrelated tool (evidence below: a desktop-commanderstart_processresult landed inside atool_searchpart).Expected behavior
The send proceeds; at worst the malformed part is rendered with a fallback ("No tools matched…").
Actual behavior
TypeError: Cannot read properties of undefined (reading 'length')
at formatSearchForModel (main.js:128337)
at Object.toModelOutput (main.js:128332)
at createToolModelOutput (main.js:46383)
at processBlock (main.js:51549)
at convertToModelMessages (main.js:51570)
TEXT
The topic becomes unusable; every retry stores a
data-errorpart and fails again.Root cause (from app.asar)
Additional Context
No response