Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/kimi_cli/soul/compaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ def prepare(
TextPart(text=f"## Message {i + 1}\nRole: {msg.role}\nContent:\n")
)
compact_message.content.extend(
part for part in msg.content if isinstance(part, TextPart)
part for part in msg.content
if isinstance(part, TextPart) and part.text.strip()
)
prompt_text = "\n" + prompts.COMPACT
if custom_instruction:
Expand All @@ -186,4 +187,11 @@ def prepare(
f"{custom_instruction}"
)
compact_message.content.append(TextPart(text=prompt_text))
# Defensive: if after filtering there are no non-empty text parts besides
# headers and the prompt, skip compaction to avoid API 400 "text content is empty".
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)
Comment on lines +192 to +196

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.

return self.PrepareResult(compact_message=compact_message, to_preserve=to_preserve)
Loading