Skip to content

fix: prevent Kimi session poisoning from empty MCP tool-result text#14174

Open
yxshee wants to merge 2 commits intoanomalyco:devfrom
yxshee:update/figma-mcp-empty-text-kimi
Open

fix: prevent Kimi session poisoning from empty MCP tool-result text#14174
yxshee wants to merge 2 commits intoanomalyco:devfrom
yxshee:update/figma-mcp-empty-text-kimi

Conversation

@yxshee
Copy link
Copy Markdown
Contributor

@yxshee yxshee commented Feb 18, 2026

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

  1. In packages/opencode/src/session/prompt.ts, MCP screenshot/image-only responses could persist output: "" when no text was present.
  2. In packages/opencode/src/session/message-v2.ts, that empty output was replayed into tool-result messages.
  3. For OpenAI-compatible Kimi k2.5, empty tool text is rejected, producing "Text content is empty".
  4. The invalid tool-result remained in history, so each subsequent request replayed the same invalid part and appeared to "poison" the session.

Fix approach

  • Added Kimi k2.5-specific tool-result normalization in ProviderTransform:
    • isKimiK25(...)
    • KIMI_TOOL_RESULT_FALLBACK_TEXT
    • Rewrite empty tool-result text to a non-empty fallback for Kimi.
  • Added MCP normalization helpers in SessionPrompt:
    • normalizeMcpResultContent(...)
    • normalizeMcpOutput(...)
    • If image/resource blob exists and text is empty for Kimi, inject non-empty fallback text.
    • If tool response is error with empty text, inject non-empty error fallback.
  • Added debug flag OPENCODE_DEBUG_MCP_PAYLOAD in Flag and debug-safe MCP payload shape logging (no raw base64 content).
  • Added replay-time healing in MessageV2.toModelMessages(...):
    • Kimi empty completed tool outputs normalized to fallback.
    • Empty tool error text normalized to fallback.
  • Added repeated-identical-tool-error circuit breaker in SessionProcessor:
    • Tracks tool + input + error signatures.
    • Stops after 3 repeats and appends actionable assistant text.

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.ts
    • Kimi image-only tool-result gets non-empty fallback.
    • Whitespace-only tool-result text with image does not produce empty payload.
    • Empty tool-error text gets non-empty fallback.
  • packages/opencode/test/provider/transform.test.ts
    • Kimi-only empty tool-result rewrite.
    • Non-Kimi OpenAI-compatible guard (no rewrite).
  • packages/opencode/test/session/kimi-session-regression.test.ts
    • Regression covering a prior empty screenshot result followed by a valid screenshot in the same history.
  • packages/opencode/test/session/prompt-mcp-normalization.test.ts
    • Figma-like image and resource.blob schema normalization cases.

How did you verify your code works?

Automated:

  • cd packages/opencode && bun test test/session/message-v2.test.ts
  • cd packages/opencode && bun test test/provider/transform.test.ts
  • cd packages/opencode && bun test test/session/kimi-session-regression.test.ts
  • cd packages/opencode && bun test test/session/prompt-mcp-normalization.test.ts
  • cd packages/opencode && bun run typecheck

Manual verification steps (for macOS 26.1 + iTerm2 + Figma Desktop MCP) included in issue workflow:

  1. Set model to Kimi k2.5.
  2. Run figma-desktop_get_design_context then figma-desktop_get_screenshot on a valid node.
  3. Confirm no empty-text session poisoning occurs and subsequent tool calls continue.

Release notes

  • Fix: Prevent Kimi k2.5 sessions from getting stuck after MCP screenshot/image-only tool results by normalizing empty tool-result text and adding repeat-error circuit breaking.

Fixes #14107

Copilot AI review requested due to automatic review settings February 18, 2026 18:22
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_PAYLOAD for 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.

Comment on lines +228 to +241
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,
})
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Counts repeated identical tool errors (same tool + input + error)
  2. Stops execution after the threshold is reached
  3. Appends the expected message to the user
  4. Resets the counter for different tool/input/error combinations

Copilot uses AI. Check for mistakes.
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))
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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))

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: "Text content is empty" error when using figma mcp with Kimi k 2.5 Kimi for coding

2 participants