Skip to content

fix(compaction): filter empty text parts to avoid API 400#2395

Open
rastinder wants to merge 1 commit into
MoonshotAI:mainfrom
rastinder:fix-compaction-empty-text-parts
Open

fix(compaction): filter empty text parts to avoid API 400#2395
rastinder wants to merge 1 commit into
MoonshotAI:mainfrom
rastinder:fix-compaction-empty-text-parts

Conversation

@rastinder
Copy link
Copy Markdown

@rastinder rastinder commented May 29, 2026

Summary

Context compaction fails with Moonshot API error text content is empty when historical messages contain empty or whitespace-only TextParts. This is the same class of bug that was fixed for tool messages in #1663, but the compaction path was missed.

Changes

  • Filter out TextParts with empty/whitespace-only text when building the compaction prompt.
  • Skip compaction entirely if no meaningful text remains, falling back to preserving the full context.

Reproduction

  1. Start a session that accumulates enough context to trigger auto-compaction.
  2. Ensure some messages have empty TextParts (e.g. tool results with no text output).
  3. Run kimi export.
  4. Compaction begins and crashes with:
APIStatusError: Error code: 400 - text content is empty

Fix

Two defensive changes in SimpleCompaction.prepare():

# 1. Filter empty text parts
compact_message.content.extend(
    part for part in msg.content
    if isinstance(part, TextPart) and part.text.strip()
)

# 2. Skip compaction if nothing meaningful remains
if not any(
    isinstance(p, TextPart) and p.text.strip()
    for p in compact_message.content
):
    return self.PrepareResult(compact_message=None, to_preserve=messages)

Testing

  • The fix is purely defensive and preserves existing behavior for non-empty content.
  • When empty content is detected, compaction is gracefully skipped rather than crashing.

Open in Devin Review

Context compaction fails with Moonshot API error 'text content is empty'
when historical messages contain empty or whitespace-only TextParts.

This applies the same defensive guard already used for tool messages
(MoonshotAI#1663) to the compaction path:

1. Filter out TextParts with empty/whitespace-only text when building
   the compaction prompt.
2. Skip compaction entirely if no meaningful text remains, falling back
   to preserving the full context.

Fixes: context compaction crash during 'kimi export'
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines +192 to +196
if not any(
isinstance(p, TextPart) and p.text.strip()
for p in compact_message.content
):
return self.PrepareResult(compact_message=None, to_preserve=messages)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Defensive empty-content check is always True and never triggers

The defensive check at lines 192-196 is meant to skip compaction when all actual message content is empty (to avoid an API 400 error). However, it checks whether any TextPart in compact_message.content has non-empty .strip() — and this will always be True because compact_message.content always contains:

  1. Header parts like TextPart(text="## Message 1\nRole: user\nContent:\n") (appended at line 175 for every message in to_compact, which is guaranteed non-empty by the check at line 167)
  2. The prompt part TextPart(text="\n" + prompts.COMPACT) (appended at line 189, where prompts.COMPACT is a ~73-line markdown document per src/kimi_cli/prompts/compact.md)

Both of these always pass the p.text.strip() test, so not any(...) is always False, and the early return on line 196 is dead code. The exact edge case this was supposed to guard against (messages whose content is all empty/whitespace TextParts or entirely non-text parts) will still send them to the API and trigger the 400 error.

The check should exclude header and prompt parts

The comment says "no non-empty text parts besides headers and the prompt" but the code doesn't actually exclude headers and the prompt from the check. A correct implementation would track whether any actual content parts were added, e.g. with a counter or flag in the loop at lines 173-180.

Prompt for agents
The defensive check on lines 192-196 in src/kimi_cli/soul/compaction.py is dead code because it checks for any non-empty TextPart across all of compact_message.content, but this always includes non-empty header TextParts (line 175) and the prompt TextPart (line 189).

The intent (per the comment on line 190-191) is to detect when the actual message content parts are all empty/missing, i.e. when the only TextParts are the headers and the prompt.

A clean fix would be to track whether any actual content was added during the loop at lines 173-180. For example, add a boolean flag:

  has_content = False
  for i, msg in enumerate(to_compact):
      compact_message.content.append(
          TextPart(text=f"## Message {i + 1}\nRole: {msg.role}\nContent:\n")
      )
      content_parts = [
          part for part in msg.content
          if isinstance(part, TextPart) and part.text.strip()
      ]
      if content_parts:
          has_content = True
      compact_message.content.extend(content_parts)

Then replace the check at lines 192-196 with:

  if not has_content:
      return self.PrepareResult(compact_message=None, to_preserve=messages)
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant