fix(compaction): filter empty text parts to avoid API 400#2395
fix(compaction): filter empty text parts to avoid API 400#2395rastinder wants to merge 1 commit into
Conversation
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'
| 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) |
There was a problem hiding this comment.
🔴 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:
- Header parts like
TextPart(text="## Message 1\nRole: user\nContent:\n")(appended at line 175 for every message into_compact, which is guaranteed non-empty by the check at line 167) - The prompt part
TextPart(text="\n" + prompts.COMPACT)(appended at line 189, whereprompts.COMPACTis a ~73-line markdown document persrc/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)
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Context compaction fails with Moonshot API error
text content is emptywhen historical messages contain empty or whitespace-onlyTextParts. This is the same class of bug that was fixed for tool messages in#1663, but the compaction path was missed.Changes
TextParts with empty/whitespace-only text when building the compaction prompt.Reproduction
TextParts (e.g. tool results with no text output).kimi export.Fix
Two defensive changes in
SimpleCompaction.prepare():Testing