Skip to content
Open
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Only write entries that are worth mentioning to users.

## Unreleased

- Print: Wait for background tasks before exiting — in one-shot `--print` mode, the process now waits for running background agents to finish and lets the model process their results, instead of exiting and killing them
- Core: Fix agent loop silently stopping when model response contains only thinking content — detect think-only responses (reasoning content with no text or tool calls) as an incomplete response error and retry automatically
- Core: Fix crash on streaming mid-flight network disconnection — when the OpenAI SDK raises a base `APIError` (instead of `APIConnectionError`) during long-running streams, the error is now correctly classified as retryable, enabling automatic retry and connection recovery instead of an unrecoverable crash
- Shell: Exclude empty current session from `/sessions` picker — completely empty sessions (no conversation history and no custom title) are no longer shown in the session list; sessions with a custom title are still displayed
Expand Down
1 change: 1 addition & 0 deletions docs/en/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ This page documents the changes in each Kimi Code CLI release.

## Unreleased

- Print: Wait for background tasks before exiting — in one-shot `--print` mode, the process now waits for running background agents to finish and lets the model process their results, instead of exiting and killing them
- Core: Fix agent loop silently stopping when model response contains only thinking content — detect think-only responses (reasoning content with no text or tool calls) as an incomplete response error and retry automatically
- Core: Fix crash on streaming mid-flight network disconnection — when the OpenAI SDK raises a base `APIError` (instead of `APIConnectionError`) during long-running streams, the error is now correctly classified as retryable, enabling automatic retry and connection recovery instead of an unrecoverable crash
- Shell: Exclude empty current session from `/sessions` picker — completely empty sessions (no conversation history and no custom title) are no longer shown in the session list; sessions with a custom title are still displayed
Expand Down
1 change: 1 addition & 0 deletions docs/zh/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

## 未发布

- Print:退出前等待后台任务完成——在单次 `--print` 模式下,进程现在会等待仍在运行的后台 Agent 完成并让模型处理它们的结果,而不是直接退出并杀死它们
- Core:修复模型响应仅包含思考内容时 agent loop 静默停止的问题——将仅含思考内容(无文本或工具调用)的响应检测为不完整响应错误并自动重试
- Core:修复长时间 streaming 过程中网络断连导致崩溃的问题——当 OpenAI SDK 在流式传输中途抛出基类 `APIError`(而非 `APIConnectionError`)时,现在能正确识别为可重试错误,自动触发重试和连接恢复,而不再直接崩溃退出
- Shell:从 `/sessions` 选择器中排除空的当前会话——完全为空的会话(既无对话记录也无自定义标题)不再显示在会话列表中;有自定义标题的会话仍然正常显示
Expand Down
8 changes: 8 additions & 0 deletions src/kimi_cli/background/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ def _active_task_count(self) -> int:
1 for view in self._store.list_views() if not is_terminal_status(view.runtime.status)
)

def has_active_tasks(self) -> bool:
"""Return True if any background tasks are in a non-terminal status.

This includes ``running``, ``awaiting_approval``, and any other
non-terminal state — not just actively executing tasks.
"""
return self._active_task_count() > 0

def _worker_command(self, task_dir: Path) -> list[str]:
if getattr(sys, "frozen", False):
return [
Expand Down
1 change: 1 addition & 0 deletions src/kimi_cli/soul/kimisoul.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,7 @@ async def _agent_loop(self) -> TurnOutcome:
has_steers = await self._consume_pending_steers()
if has_steers:
continue # steers injected, force another LLM step

final_message = (
step_outcome.assistant_message
if step_outcome.stop_reason == "no_tool_calls"
Expand Down
68 changes: 68 additions & 0 deletions src/kimi_cli/ui/print/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,74 @@ def _handler():
runtime.session.wire_file if runtime else None,
runtime,
)

# In one-shot text mode the process exits after this
# function returns, which would kill still-running
# background agents. Poll until they finish, calling
# reconcile() each iteration (the notification pump
# inside run_soul is no longer running, so we must
# drive reconcile ourselves to recover lost workers
# and publish terminal notifications). Only re-enter
# the soul when there are pending LLM notifications.
#
# stream-json mode is multi-turn: background tasks
# from one command must not block the next command.
#
# keep_alive_on_exit opts into "fire and forget"
# semantics: background tasks are meant to outlive
# the CLI process, so Print must not block waiting
# for them.
if (
runtime
and runtime.role == "root"
and self.input_format == "text"
and not runtime.config.background.keep_alive_on_exit
):
manager = runtime.background_tasks
notifications = runtime.notifications
while not cancel_event.is_set():
# Drive reconcile() ourselves: the notification
# pump inside run_soul is no longer running, so
# we must recover lost workers and publish
# terminal notifications here.
manager.reconcile()
if notifications.has_pending_for_sink("llm"):
# Re-enter soul so the LLM can process the
# completion notification. Do this even if
# other tasks are still active — progress on
# completed tasks should not wait on siblings.
bg_prompt = (
"<system-reminder>"
"Background tasks have completed."
" Process their results."
"</system-reminder>"
)
await run_soul(
self.soul,
bg_prompt,
partial(visualize, self.output_format, self.final_only),
cancel_event,
runtime.session.wire_file,
runtime,
)
continue
if not manager.has_active_tasks():
# Re-check once after noticing no active
# tasks: a worker may have finished between
# the reconcile above and this snapshot,
# leaving a terminal state on disk that we
# haven't published yet. Without this
# second reconcile+pending check, that
# final completion notification would be
# lost when the process exits.
manager.reconcile()
if notifications.has_pending_for_sink("llm"):
continue
break
# Still waiting for tasks to finish.
await asyncio.sleep(1.0)
if cancel_event.is_set():
raise RunCancelled
else:
logger.info("Empty command, skipping")

Expand Down
Loading
Loading