diff --git a/CHANGELOG.md b/CHANGELOG.md index a22dd1b4e..fba004feb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Only write entries that are worth mentioning to users. ## Unreleased +- Core: Fix context compaction failing when conversation contains media parts (images, audio, video) — switch from blacklist filtering (exclude `ThinkPart`) to whitelist filtering (only keep `TextPart`) to prevent unsupported content types from being sent to the compaction API - Web: Fix `@` file mention index not refreshing after switching sessions or when workspace files change — reset index on session switch, auto-refresh after 30s staleness, and support path-prefix search beyond the 500-file limit ## 1.19.0 (2026-03-10) diff --git a/docs/en/release-notes/changelog.md b/docs/en/release-notes/changelog.md index 240f8e692..38a27e478 100644 --- a/docs/en/release-notes/changelog.md +++ b/docs/en/release-notes/changelog.md @@ -4,6 +4,7 @@ This page documents the changes in each Kimi Code CLI release. ## Unreleased +- Core: Fix context compaction failing when conversation contains media parts (images, audio, video) — switch from blacklist filtering (exclude `ThinkPart`) to whitelist filtering (only keep `TextPart`) to prevent unsupported content types from being sent to the compaction API - Web: Fix `@` file mention index not refreshing after switching sessions or when workspace files change — reset index on session switch, auto-refresh after 30s staleness, and support path-prefix search beyond the 500-file limit ## 1.19.0 (2026-03-10) diff --git a/docs/zh/release-notes/changelog.md b/docs/zh/release-notes/changelog.md index aefebaed1..b61baf961 100644 --- a/docs/zh/release-notes/changelog.md +++ b/docs/zh/release-notes/changelog.md @@ -4,6 +4,7 @@ ## 未发布 +- Core:修复对话包含媒体内容(图片、音频、视频)时上下文压缩失败的问题——将过滤策略从黑名单(排除 `ThinkPart`)改为白名单(仅保留 `TextPart`),防止不支持的内容类型被发送到压缩 API - Web:修复 `@` 文件提及索引在切换会话或工作区文件变更后不刷新的问题——切换会话时重置索引,30 秒过期自动刷新,输入路径前缀可查找超出 500 文件上限的文件 ## 1.19.0 (2026-03-10) diff --git a/src/kimi_cli/soul/compaction.py b/src/kimi_cli/soul/compaction.py index 7742806be..7db37d5d8 100644 --- a/src/kimi_cli/soul/compaction.py +++ b/src/kimi_cli/soul/compaction.py @@ -175,7 +175,7 @@ def prepare( TextPart(text=f"## Message {i + 1}\nRole: {msg.role}\nContent:\n") ) compact_message.content.extend( - part for part in msg.content if not isinstance(part, ThinkPart) + part for part in msg.content if isinstance(part, TextPart) ) prompt_text = "\n" + prompts.COMPACT if custom_instruction: diff --git a/tests/core/test_simple_compaction.py b/tests/core/test_simple_compaction.py index 40473d7d9..b2fa80dbb 100644 --- a/tests/core/test_simple_compaction.py +++ b/tests/core/test_simple_compaction.py @@ -2,7 +2,7 @@ from inline_snapshot import snapshot from kosong.chat_provider import TokenUsage -from kosong.message import Message +from kosong.message import AudioURLPart, ImageURLPart, Message, VideoURLPart import kimi_cli.prompts as prompts from kimi_cli.soul.compaction import CompactionResult, SimpleCompaction, should_auto_compact @@ -213,3 +213,64 @@ def test_custom_ratio_triggers_earlier(self): def test_zero_tokens_never_triggers(self): """Empty context should never trigger compaction.""" assert not should_auto_compact(0, 200_000, trigger_ratio=0.85, reserved_context_size=50_000) + + +def test_prepare_only_keeps_text_parts_in_compaction(): + """Compaction input should only contain TextPart (whitelist approach). + + Non-text parts (media, think, etc.) are filtered out because the compaction + API endpoint only supports text content. + + Fixes: https://github.com/MoonshotAI/kimi-cli/issues/1395 + Fixes: https://github.com/MoonshotAI/kimi-cli/issues/1390 + """ + messages = [ + Message( + role="user", + content=[ + TextPart(text="Analyze these files:"), + ImageURLPart(image_url=ImageURLPart.ImageURL(url="data:image/png;base64,IMG")), + AudioURLPart(audio_url=AudioURLPart.AudioURL(url="data:audio/mp3;base64,AUD")), + VideoURLPart(video_url=VideoURLPart.VideoURL(url="data:video/mp4;base64,VID")), + ThinkPart(think="internal reasoning"), + ], + ), + Message(role="assistant", content=[TextPart(text="I can see all the media files.")]), + Message(role="user", content=[TextPart(text="What's your conclusion?")]), + ] + + result = SimpleCompaction(max_preserved_messages=1).prepare(messages) + + assert result.compact_message is not None + # Verify only TextPart remains in the compaction request + for part in result.compact_message.content: + assert isinstance(part, TextPart), ( + f"Only TextPart should be in compaction input, got {type(part).__name__}" + ) + + # Text content should be preserved + texts = [p.text for p in result.compact_message.content if isinstance(p, TextPart)] + assert any("Analyze these files:" in t for t in texts) + assert any("I can see all the media files." in t for t in texts) + + +def test_prepare_preserves_media_parts_in_recent_messages(): + """Media parts in preserved (recent) messages should remain untouched.""" + messages = [ + Message(role="user", content=[TextPart(text="Old question")]), + Message(role="assistant", content=[TextPart(text="Old answer")]), + Message( + role="user", + content=[ + TextPart(text="Look at this video:"), + VideoURLPart(video_url=VideoURLPart.VideoURL(url="data:video/mp4;base64,VID")), + ], + ), + Message(role="assistant", content=[TextPart(text="Nice video!")]), + ] + + result = SimpleCompaction(max_preserved_messages=2).prepare(messages) + + # Preserved messages should keep their media parts intact + preserved_user_msg = result.to_preserve[0] + assert any(isinstance(p, VideoURLPart) for p in preserved_user_msg.content)