Skip to content
Merged
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

- Core: Fix JSON parsing error when LLM tool call arguments contain unescaped control characters — use `json.loads(strict=False)` across all LLM output parsing paths to prevent tool execution failure and session corruption
- Shell: Auto-trigger agent when background tasks complete while idle — the shell now detects when a background bash command or agent task finishes and automatically starts a new agent turn to process the results, instead of waiting for the user to type something
- Core: Fix `QuestionRequest` hanging in print mode — `AskUserQuestion`, `EnterPlanMode`, and `ExitPlanMode` now auto-resolve when running in non-interactive (yolo) mode, preventing indefinite tool call hangs in `--print` sessions
- Core: Fix background agent task output not visible until completion — `/task` browser and `TaskOutput` tool now show real-time output while background agent tasks are running, by tee-writing to the task log during execution instead of copying on completion
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

- Core: Fix JSON parsing error when LLM tool call arguments contain unescaped control characters — use `json.loads(strict=False)` across all LLM output parsing paths to prevent tool execution failure and session corruption
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.

🟡 Manual edit to auto-generated English changelog violates docs/AGENTS.md rule

The docs/AGENTS.md rule states: "The English changelog (docs/en/release-notes/changelog.md) is auto-generated from the root CHANGELOG.md. Do not edit it manually." This PR directly adds a new entry to docs/en/release-notes/changelog.md, which should instead be generated by running npm run sync from the docs/ directory after editing the root CHANGELOG.md. The content is identical to what the sync script would produce, so there is no functional impact, but it violates the stated workflow.

Prompt for agents
Remove the manually added line from docs/en/release-notes/changelog.md and instead run `npm run sync` from the docs/ directory to auto-generate the English changelog from the root CHANGELOG.md. The sync script (docs/scripts/sync-changelog.mjs) will copy the entry that was already added to the root CHANGELOG.md.
Open in Devin Review

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

- Shell: Auto-trigger agent when background tasks complete while idle — the shell now detects when a background bash command or agent task finishes and automatically starts a new agent turn to process the results, instead of waiting for the user to type something
- Core: Fix `QuestionRequest` hanging in print mode — `AskUserQuestion`, `EnterPlanMode`, and `ExitPlanMode` now auto-resolve when running in non-interactive (yolo) mode, preventing indefinite tool call hangs in `--print` sessions
- Core: Fix background agent task output not visible until completion — `/task` browser and `TaskOutput` tool now show real-time output while background agent tasks are running, by tee-writing to the task log during execution instead of copying on completion
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 @@

## 未发布

- Core:修复 LLM 工具调用参数包含未转义控制字符时 JSON 解析失败的问题——在所有 LLM 输出解析路径使用 `json.loads(strict=False)`,防止工具执行失败和会话永久损坏
- Shell:空闲时自动响应后台任务完成——Shell 现在会检测后台 Bash 命令或 Agent 任务的完成,并自动发起新的 Agent 轮次处理结果,无需等待用户输入
- Core:修复 Print 模式下 `QuestionRequest` 导致挂起的问题——`AskUserQuestion`、`EnterPlanMode` 和 `ExitPlanMode` 在非交互(yolo)模式下自动处理,避免 `--print` 会话中工具调用无限挂起
- Core:修复后台 Agent 任务运行期间无法查看输出的问题——`/task` 浏览器和 `TaskOutput` 工具现在可实时显示后台 Agent 任务的输出,通过在执行期间同步写入任务日志替代完成后拷贝的方式实现
Expand Down
1 change: 1 addition & 0 deletions packages/kosong/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Core: Use `json.loads(strict=False)` when parsing tool call arguments to tolerate unescaped control characters from LLM output
- Core: Treat `httpx.ProtocolError` as `APIConnectionError` in shared `convert_httpx_error()` mapping so streaming protocol disconnects now participate in existing retry logic
- Anthropic: Fix `httpx.ReadTimeout` leaking through `_convert_stream_response` during streaming — the exception is now caught and converted to `APITimeoutError`, enabling retry logic that was previously bypassed
- Anthropic: Fix `_convert_error` ordering — `AnthropicAPITimeoutError` is now checked before `AnthropicAPIConnectionError` to avoid misclassification due to inheritance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ def _convert_message(self, message: Message) -> MessageParam:
for tool_call in message.tool_calls or []:
if tool_call.function.arguments:
try:
parsed_arguments = json.loads(tool_call.function.arguments)
parsed_arguments = json.loads(tool_call.function.arguments, strict=False)
except json.JSONDecodeError as exc: # pragma: no cover - defensive guard
raise ChatProviderError("Tool call arguments must be valid JSON.") from exc
if not isinstance(parsed_arguments, dict):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,7 @@ def message_to_google_genai(message: Message) -> Content:
for tool_call in message.tool_calls or []:
if tool_call.function.arguments:
try:
parsed_arguments = json.loads(tool_call.function.arguments)
parsed_arguments = json.loads(tool_call.function.arguments, strict=False)
except json.JSONDecodeError as exc: # pragma: no cover - defensive guard
raise ChatProviderError("Tool call arguments must be valid JSON.") from exc
if not isinstance(parsed_arguments, dict):
Expand Down
2 changes: 1 addition & 1 deletion packages/kosong/src/kosong/contrib/context/linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def _restore():
for line in f:
if not line.strip():
continue
line_json = json.loads(line)
line_json = json.loads(line, strict=False)
if "token_count" in line_json:
self._token_count = line_json["token_count"]
continue
Expand Down
2 changes: 1 addition & 1 deletion packages/kosong/src/kosong/tooling/simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def handle(self, tool_call: ToolCall) -> HandleResult:
tool = self._tool_dict[tool_call.function.name]

try:
arguments: JsonType = json.loads(tool_call.function.arguments or "{}")
arguments: JsonType = json.loads(tool_call.function.arguments or "{}", strict=False)
except json.JSONDecodeError as e:
return ToolResult(tool_call_id=tool_call.id, return_value=ToolParseError(str(e)))

Expand Down
2 changes: 1 addition & 1 deletion src/kimi_cli/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def is_empty(self) -> bool:
line = line.strip()
if not line:
continue
role = json.loads(line).get("role")
role = json.loads(line, strict=False).get("role")
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

is_empty() still raises json.JSONDecodeError if any JSONL line is malformed in ways unrelated to strict (e.g., truncated/corrupted lines). Since this method is likely used in startup/housekeeping paths, a single bad line can crash the CLI. Consider catching json.JSONDecodeError (and optionally TypeError) inside the loop and skipping bad lines (or conservatively treating the session as non-empty) to make session handling more resilient.

Suggested change
role = json.loads(line, strict=False).get("role")
try:
record = json.loads(line, strict=False)
except (json.JSONDecodeError, ValueError, TypeError):
logger.warning(
"Skipping malformed JSON line in context file {file}:",
file=self.context_file,
)
continue
role = record.get("role")

Copilot uses AI. Check for mistakes.
if isinstance(role, str) and not role.startswith("_"):
return False
except FileNotFoundError:
Expand Down
4 changes: 2 additions & 2 deletions src/kimi_cli/soul/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ async def restore(self) -> bool:
async for line in f:
if not line.strip():
continue
line_json = json.loads(line)
line_json = json.loads(line, strict=False)
if line_json["role"] == "_system_prompt":
self._system_prompt = line_json["content"]
continue
Expand Down Expand Up @@ -160,7 +160,7 @@ async def revert_to(self, checkpoint_id: int):
if not line.strip():
continue

line_json = json.loads(line)
line_json = json.loads(line, strict=False)
if line_json["role"] == "_checkpoint" and line_json["id"] == checkpoint_id:
break

Expand Down
2 changes: 1 addition & 1 deletion src/kimi_cli/soul/toolset.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def handle(self, tool_call: ToolCall) -> HandleResult:
tool = self._tool_dict[tool_call.function.name]

try:
arguments: JsonType = json.loads(tool_call.function.arguments or "{}")
arguments: JsonType = json.loads(tool_call.function.arguments or "{}", strict=False)
except json.JSONDecodeError as e:
return ToolResult(tool_call_id=tool_call.id, return_value=ToolParseError(str(e)))

Expand Down
2 changes: 1 addition & 1 deletion src/kimi_cli/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def extract_key_argument(json_content: str | streamingjson.Lexer, tool_name: str
else:
json_str = json_content
try:
curr_args: JsonType = json.loads(json_str)
curr_args: JsonType = json.loads(json_str, strict=False)
except json.JSONDecodeError:
return None
if not curr_args:
Expand Down
2 changes: 1 addition & 1 deletion src/kimi_cli/ui/shell/debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def _format_tool_call(tool_call: ToolCall) -> Panel:
"""Format a tool call."""
args = tool_call.function.arguments or "{}"
try:
args_formatted = json.dumps(json.loads(args), indent=2)
args_formatted = json.dumps(json.loads(args, strict=False), indent=2)
args_syntax = Syntax(args_formatted, "json", theme="monokai", padding=(0, 1))
except json.JSONDecodeError:
args_syntax = Text(args, style="red")
Comment on lines 75 to 76
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

Similar to other call sites, if tool_call.function.arguments is ever a non-string truthy value, json.loads(args, ...) can raise TypeError, which is not caught here and would break the debug UI. Consider catching (json.JSONDecodeError, TypeError) to keep the debug view robust when encountering unexpected argument types.

Suggested change
except json.JSONDecodeError:
args_syntax = Text(args, style="red")
except (json.JSONDecodeError, TypeError):
args_syntax = Text(str(args), style="red")

Copilot uses AI. Check for mistakes.
Expand Down
2 changes: 1 addition & 1 deletion src/kimi_cli/ui/shell/visualize.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ def _extract_full_url(arguments: str | None, tool_name: str) -> str | None:
if tool_name != "FetchURL" or not arguments:
return None
try:
args = json.loads(arguments)
args = json.loads(arguments, strict=False)
except (json.JSONDecodeError, TypeError):
return None
if isinstance(args, dict):
Expand Down
7 changes: 4 additions & 3 deletions src/kimi_cli/utils/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def _extract_tool_call_hint(args_json: str) -> str:
short string value. Returns ``""`` when nothing useful is found.
"""
try:
parsed: object = json.loads(args_json)
parsed: object = json.loads(args_json, strict=False)
except (json.JSONDecodeError, TypeError):
return ""
if not isinstance(parsed, dict):
Expand Down Expand Up @@ -104,7 +104,8 @@ def _format_tool_call_md(tool_call: ToolCall) -> str:
title += f" (`{hint}`)"

try:
args_formatted = json.dumps(json.loads(args_raw), indent=2, ensure_ascii=False)
parsed = json.loads(args_raw, strict=False)
args_formatted = json.dumps(parsed, indent=2, ensure_ascii=False)
except json.JSONDecodeError:
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

args_raw comes from tc.function.arguments and is not guaranteed to be a str at runtime (elsewhere in this file you already defensively catch TypeError). If it’s a non-string truthy value, json.loads(args_raw, ...) will raise TypeError and bypass the current except, crashing export rendering. Consider expanding the handler to except (json.JSONDecodeError, TypeError) for consistency with _extract_tool_call_hint() and _stringify_tool_calls().

Suggested change
except json.JSONDecodeError:
except (json.JSONDecodeError, TypeError):

Copilot uses AI. Check for mistakes.
args_formatted = args_raw

Expand Down Expand Up @@ -404,7 +405,7 @@ def _stringify_tool_calls(tool_calls: Sequence[ToolCall]) -> str:
for tc in tool_calls:
args_raw = tc.function.arguments or "{}"
try:
args = json.loads(args_raw)
args = json.loads(args_raw, strict=False)
args_str = json.dumps(args, ensure_ascii=False)
except (json.JSONDecodeError, TypeError):
args_str = args_raw
Expand Down
Loading