fix: prevent Kimi session poisoning from empty MCP tool-result text#14174
fix: prevent Kimi session poisoning from empty MCP tool-result text#14174yxshee wants to merge 2 commits intoanomalyco:devfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes a Kimi k2.5 session poisoning issue where MCP screenshot tools returning image-only content with empty text caused persistent "Text content is empty" errors that rendered sessions unusable.
Changes:
- Added multi-layered normalization for empty tool-result text specifically for Kimi k2.5 models
- Implemented circuit breaker to stop auto-retrying after 3 identical tool errors
- Added debug flag
OPENCODE_DEBUG_MCP_PAYLOADfor MCP payload inspection
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/opencode/src/provider/transform.ts | Added Kimi k2.5 detection function and empty tool-result normalization at API send-time |
| packages/opencode/src/session/message-v2.ts | Added replay-time healing for empty tool outputs and errors when using Kimi |
| packages/opencode/src/session/prompt.ts | Added MCP normalization helpers for content and output with Kimi-specific fallback logic |
| packages/opencode/src/session/processor.ts | Added circuit breaker tracking identical tool errors with auto-retry limit |
| packages/opencode/src/flag/flag.ts | Added OPENCODE_DEBUG_MCP_PAYLOAD debug flag |
| packages/opencode/test/session/prompt-mcp-normalization.test.ts | Tests for MCP normalization with Figma-like image and resource blob schemas |
| packages/opencode/test/session/message-v2.test.ts | Tests for Kimi tool result normalization during message conversion |
| packages/opencode/test/session/kimi-session-regression.test.ts | Regression test for empty screenshot results followed by valid results |
| packages/opencode/test/provider/transform.test.ts | Tests for Kimi-specific transform logic and non-Kimi guard |
| packages/web/src/content/docs/troubleshooting.mdx | Added troubleshooting section for "Text content is empty" error |
| packages/web/src/content/docs/mcp-servers.mdx | Added documentation about screenshot tool output normalization |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const key = [match.tool, JSON.stringify(value.input ?? match.state.input ?? {}), errorText].join("|") | ||
| const count = (toolErrors.get(key) ?? 0) + 1 | ||
| toolErrors.set(key, count) | ||
| if (count >= TOOL_ERROR_REPEAT_THRESHOLD) { | ||
| blocked = true | ||
| await Session.updatePart({ | ||
| id: Identifier.ascending("part"), | ||
| messageID: input.assistantMessage.id, | ||
| sessionID: input.assistantMessage.sessionID, | ||
| type: "text", | ||
| text: "Stopped auto-retrying after 3 identical tool errors in this session. Verify MCP server response and retry.", | ||
| synthetic: true, | ||
| }) | ||
| } |
There was a problem hiding this comment.
The circuit breaker implementation tracks identical tool errors and stops auto-retrying after 3 occurrences, but there are no automated tests to verify this behavior. Consider adding tests to ensure the circuit breaker correctly:
- Counts repeated identical tool errors (same tool + input + error)
- Stops execution after the threshold is reached
- Appends the expected message to the user
- Resets the counter for different tool/input/error combinations
| export function isKimiK25(model: Pick<Provider.Model, "api" | "id">) { | ||
| if (model.api.npm !== "@ai-sdk/openai-compatible") return false | ||
| const id = `${model.id} ${model.api.id}`.toLowerCase() | ||
| return ["kimi-k2.5", "kimi-k2p5", "kimi-k2-5", "k2p5"].some((part) => id.includes(part)) |
There was a problem hiding this comment.
The Kimi K2.5 detection uses substring matching with the pattern "k2p5" which could potentially match non-Kimi models that happen to include this substring. While the additional check for "@ai-sdk/openai-compatible" reduces this risk, consider whether the patterns could be more specific. For example, "k2p5" might match a hypothetical model named "mock2p5" or "pk2p5test". If this is deemed too broad, consider using more anchored patterns or checking the providerID field more strictly.
| return ["kimi-k2.5", "kimi-k2p5", "kimi-k2-5", "k2p5"].some((part) => id.includes(part)) | |
| const kimiPatterns = [ | |
| /\bkimi-k2\.5\b/, | |
| /\bkimi-k2p5\b/, | |
| /\bkimi-k2-5\b/, | |
| /\bk2p5\b/, | |
| ] | |
| return kimiPatterns.some((pattern) => pattern.test(id)) |
What does this PR do?
Fixes a Kimi k2.5 + MCP screenshot regression where image-only tool results could serialize as empty tool text, triggering
"Text content is empty"and then repeatedly failing on subsequent turns.Root cause
packages/opencode/src/session/prompt.ts, MCP screenshot/image-only responses could persistoutput: ""when no text was present.packages/opencode/src/session/message-v2.ts, that empty output was replayed into tool-result messages."Text content is empty".Fix approach
ProviderTransform:isKimiK25(...)KIMI_TOOL_RESULT_FALLBACK_TEXTtool-resulttext to a non-empty fallback for Kimi.SessionPrompt:normalizeMcpResultContent(...)normalizeMcpOutput(...)OPENCODE_DEBUG_MCP_PAYLOADinFlagand debug-safe MCP payload shape logging (no raw base64 content).MessageV2.toModelMessages(...):SessionProcessor:tool + input + errorsignatures.Why Kimi was affected
Kimi k2.5 (OpenAI-compatible) is stricter about empty tool text payloads. Other providers/models either tolerate this shape or handle media/tool-result content differently.
Tests added
packages/opencode/test/session/message-v2.test.tspackages/opencode/test/provider/transform.test.tspackages/opencode/test/session/kimi-session-regression.test.tspackages/opencode/test/session/prompt-mcp-normalization.test.tsimageandresource.blobschema normalization cases.How did you verify your code works?
Automated:
cd packages/opencode && bun test test/session/message-v2.test.tscd packages/opencode && bun test test/provider/transform.test.tscd packages/opencode && bun test test/session/kimi-session-regression.test.tscd packages/opencode && bun test test/session/prompt-mcp-normalization.test.tscd packages/opencode && bun run typecheckManual verification steps (for macOS 26.1 + iTerm2 + Figma Desktop MCP) included in issue workflow:
figma-desktop_get_design_contextthenfigma-desktop_get_screenshoton a valid node.Release notes
Fixes #14107