Skip to content

fix: terminate shell process trees on timeout#2327

Open
he-yufeng wants to merge 1 commit into
MoonshotAI:mainfrom
he-yufeng:fix/shell-timeout-process-tree
Open

fix: terminate shell process trees on timeout#2327
he-yufeng wants to merge 1 commit into
MoonshotAI:mainfrom
he-yufeng:fix/shell-timeout-process-tree

Conversation

@he-yufeng
Copy link
Copy Markdown

@he-yufeng he-yufeng commented May 19, 2026

Summary

  • run foreground shell commands in their own local process group/session
  • terminate the whole shell process tree on timeout or cancellation
  • keep non-local KAOS backends compatible by accepting the new flag without changing their behavior

Fixes #2310

To verify

  • uv run ruff check packages\kaos\src\kaos\__init__.py packages\kaos\src\kaos\local.py packages\kaos\src\kaos\ssh.py src\kimi_cli\acp\kaos.py src\kimi_cli\tools\shell\__init__.py tests\tools\test_shell_bash.py tests\tools\test_shell_process_lifecycle.py
  • uv run ruff format --check packages\kaos\src\kaos\__init__.py packages\kaos\src\kaos\local.py packages\kaos\src\kaos\ssh.py src\kimi_cli\acp\kaos.py src\kimi_cli\tools\shell\__init__.py tests\tools\test_shell_bash.py tests\tools\test_shell_process_lifecycle.py
  • New-Item -ItemType Directory -Force .tmp\pytest-temp | Out-Null; $env:TMP=(Resolve-Path .tmp\pytest-temp).Path; $env:TEMP=$env:TMP; uv run pytest tests\tools\test_shell_process_lifecycle.py packages\kaos\tests\test_local_kaos.py -q -k "foreground_shell_uses_new_session or exec" -o cache_dir=.tmp\pytest-cache
  • python -m py_compile packages\kaos\src\kaos\__init__.py packages\kaos\src\kaos\local.py packages\kaos\src\kaos\ssh.py src\kimi_cli\acp\kaos.py src\kimi_cli\tools\shell\__init__.py tests\tools\test_shell_bash.py tests\tools\test_shell_process_lifecycle.py

Open in Devin Review

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +273 to +281
if pid > 0 and self._on_windows:
await asyncio.to_thread(
subprocess.run,
["taskkill", "/PID", str(pid), "/T", "/F"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
)
elif pid > 0 and os.name != "nt":
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.

🟡 _terminate_shell_process uses self._on_windows instead of os.name for platform detection, mismatching process creation

_terminate_shell_process checks self._on_windows (derived from environment.os_kind) to decide the termination strategy, but the process was created in packages/kaos/src/kaos/local.py:182 using os.name == "nt" to decide whether to set CREATE_NEW_PROCESS_GROUP (Windows) or start_new_session (Unix). The existing background worker at src/kimi_cli/background/worker.py:94 correctly uses os.name == "nt" consistently for both creation and termination.

If self._on_windows ever disagrees with os.name (e.g., a test constructs a Shell with os_kind="Windows" on Linux, or an SSH-backed scenario), _terminate_shell_process would try to run taskkill on a Unix machine. Since the taskkill path has no exception handling (unlike the Unix os.killpg path which catches OSError), subprocess.run(["taskkill", ...]) would raise FileNotFoundError, which propagates uncaught from _terminate_shell_process and replaces the original CancelledError/TimeoutError — leaving the process group alive and producing an unexpected exception type.

Suggested change
if pid > 0 and self._on_windows:
await asyncio.to_thread(
subprocess.run,
["taskkill", "/PID", str(pid), "/T", "/F"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
)
elif pid > 0 and os.name != "nt":
if pid > 0 and os.name == "nt":
await asyncio.to_thread(
subprocess.run,
["taskkill", "/PID", str(pid), "/T", "/F"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
)
elif pid > 0 and os.name != "nt":
Open in Devin Review

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shell tool timeout does not terminate child processes

1 participant