Skip to content

[Bug]: 公共 Environment 块(三帖共用) #19465

Description

@ZOOFOOO

Issue Checklist

  • I understand that issues are for feedback and problem solving, not for complaining in the comment section, and will provide as much information as possible to help solve the problem.
  • My issue is not listed in the FAQ.
  • I've looked at pinned issues and searched for existing Open Issues, Closed Issues, and Discussions, no similar issue or discussion was found.
  • I've filled in short, clear headings so that developers can quickly identify a rough idea of what to expect when flipping through the list of issues. And not "a suggestion", "stuck", etc.
  • I've confirmed that I am using the latest version of Cherry Studio.

Platform

Linux

Version

公共 Environment 块(三帖共用)

Bug Description

公共 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)

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

Additional Context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions