Skip to content

fix: terminal hang on exit and MCP connection leak #1984

Description

@thecannabisapp

问题描述 / Issue Description

在使用较长会话后,执行 /exit 或输入 exit 时,终端会无响应并挂起,需强制关闭终端窗口。同时,退出后系统防火墙会报告来自已终止进程的孤立 MCP 连接。

After extended sessions, executing /exit or typing exit causes the terminal to hang and become unresponsive, requiring the terminal window to be force-closed. Additionally, the system firewall reports orphaned MCP connections from terminated processes after exit.

根本原因 / Root Cause

1. TTY 阻塞导致退出挂起 / TTY Block Causes Exit Hang

_cursor_position_unix()tty.setcbreak(fd) 之后调用 os.read(fd, 32)。虽然上游代码已引入 select.select() 超时,但在以下情况 os.read() 仍可能阻塞:

  • asyncio 任务取消信号无法中断 os.read() 系统调用
  • prompt_toolkit 的 stdin reader 与 _cursor_position_unix() 竞争读取 CPR 响应时,数据可能在 select 返回后到 os.read 执行前被消耗

_cursor_position_unix() calls os.read(fd, 32) after tty.setcbreak(fd). Although upstream code already introduced a select.select() timeout, os.read() can still block because:

  • asyncio task cancellation cannot interrupt the os.read() system call
  • prompt_toolkit's stdin reader races with _cursor_position_unix() for the CPR response; data may be consumed between select returning and os.read executing

2. TTY 恢复不完整 / Incomplete TTY Restoration

ensure_tty_sane() 恢复 c_lflag 中的 ISIG | IEXTEN | ICANON | ECHO,但未重置 c_cc 中的 VMIN/VTIME。若进程在 cbreak 模式下崩溃或挂起,终端会保持在 VMIN=1, VTIME=0 状态,导致 Ctrl+C 发送字节 0x03 而非 SIGINT。

ensure_tty_sane() restores ISIG | IEXTEN | ICANON | ECHO in c_lflag, but does not reset VMIN/VTIME in c_cc. If the process crashes or hangs in cbreak mode, the terminal remains in VMIN=1, VTIME=0 state, causing Ctrl+C to send byte 0x03 instead of SIGINT.

3. MCP 客户端泄漏 / MCP Client Leak

KimiToolset.cleanup()(负责关闭所有 fastmcp.Client 连接)在 KimiCLI.shutdown_background_tasks() 或 shell 拆卸路径中从未被调用。这导致 stdio/WebSocket 传输在 CLI 进程退出后仍保持连接。

KimiToolset.cleanup(), which closes all fastmcp.Client connections, is never invoked in KimiCLI.shutdown_background_tasks() or the shell teardown path. This leaves stdio/WebSocket transports connected after the CLI process exits.

修复方案 / Proposed Fix

  1. _cursor_position_unix() 中使用 os.set_blocking(fd, False) 使读取非阻塞,并在 finally 中恢复

  2. ensure_tty_sane() 中重置 VMIN=1, VTIME=0

  3. shutdown_background_tasks() 中调用 KimiToolset.cleanup()

  4. Use os.set_blocking(fd, False) in _cursor_position_unix() to make reads non-blocking, restoring it in finally

  5. Reset VMIN=1, VTIME=0 in ensure_tty_sane()

  6. Call KimiToolset.cleanup() in shutdown_background_tasks()

环境 / Environment

  • macOS 15.7.3 (Ghostty 1.3.1)
  • Python 3.13.7
  • kimi-cli 1.37.0
  • prompt-toolkit 3.0.52

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions