Skip to content

Terminal escape sequences (CSI/OSC) leak into stdin and trigger false "Cancelled by user" abort #2181

Description

@yang-neu

What version of Kimi Code is running?

0.29.1 (kimi --version)

Which open platform/subscription were you using?

N/A — bug is in the local CLI input handling, independent of model/platform.

Which model were you using?

kimi-code/kimi-for-coding (K2.7 Coding), but the bug is model-independent.

What platform is your computer?

Remote Linux host (Ubuntu 22.04.5 LTS, x86_64) accessed via SSH from macOS.

What issue are you seeing?

When a child process (e.g. yazi --version) crashes due to a dynamic-link error, the terminal sends control sequences (CSI/OSC) to synchronize state. Kimi Code's stdin reader captures these sequences and treats them as user input, which triggers an immediate cancelAll() abort of the currently executing turn. The CLI then displays "Interrupted by user" even though the user never pressed Ctrl+C or sent any input.

Concrete symptom

• Used Shell (which yazi && yazi --version && ya --version)
  Interrupted by user

The user did not interrupt. The interruption was caused by terminal escape sequences leaking into the user-input stream.

What steps can reproduce the bug?

  1. SSH into a Linux host from a terminal that supports Kitty keyboard protocol (e.g. Ghostty, Kitty, WezTerm).
  2. Run kimi on the remote host.
  3. Ask kimi to install a TUI binary whose .deb is compiled against a newer glibc than the host has (e.g. yazi v26.5.6 .deb on Ubuntu 22.04 with glibc 2.35).
  4. kimi downloads and installs the .deb via dpkg, then runs yazi --version to verify.
  5. yazi crashes with:
    yazi: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.39' not found (required by yazi)
    
  6. The child process exits with code 1. The terminal emulator detects the TTY state change and automatically sends sync/query sequences.
  7. Bug: These sequences appear in kimi's stdin and are interpreted as a new user message, causing the active turn to be cancelled.

Evidence from logs

Terminal control sequences captured as user input

In ~/.kimi/logs/kimi.log, a prior session shows the exact bytes that were captured:

2026-07-25 15:25:59 | INFO | ...run_soul_command:827 |
  Running soul with user input: [TextPart(type='text', text='a\[6;18;8t]11;rgb:1515/1919/1e1e[?1;2;4c[6;18;8t]11;rgb:1515/1919/1e1e什么情况,我是指安装https://github.com/sxyazi/yaziGi=31;OK')]

The prefix contains literal CSI sequences:

  • ESC[6;18;8t — terminal window size report/query
  • 11;rgb:1515/1919/1e1e — OSC color response
  • ESC[?1;2;4c — DA (device attributes) response

These were not typed by the user — they were injected by the SSH client/terminal emulator after the child process crashed.

The "cancel" log line

2026-07-25 15:28:36 | INFO | ...run_soul_command:1016 |  - Cancelled by user

This happened at the exact moment after yazi --version crashed, with no human input in between.

Wire log confirms no ToolResult for the verification command

In wire.jsonl for session 23ed76c0-bb1f-4327-84be-7a92cdcbb1ee:

{"type": "ToolCall", ... "function": {"name": "Shell", "arguments": "{\"command\": \"which yazi && yazi --version && ya --version\", \"timeout\": 30}"}}
{"type": "TurnEnd", "payload": {}}

There is no ToolResult event after the ToolCall. The turn was aborted before the shell command could complete and return its output.

Root cause analysis

  1. Child process crash changes TTY state: yazi is a TUI application. Even --version may initialize terminal modes (or the dynamic-linker crash path leaves the TTY in an inconsistent state).
  2. Terminal emulator sends sync sequences: When it detects the child has exited, the terminal sends CSI/OSC sequences to query/restore state.
  3. kimi's stdin reader captures raw bytes: The sequences arrive on stdin alongside or immediately after the shell command's output stream closes.
  4. Input parser treats them as user text: The sequences are not filtered out; they become a TextPart user message.
  5. New user message triggers cancelAll(): kimi sees "new user input" while a turn is in progress, aborts the active turn, and marks it as "Cancelled by user".

What is the expected behavior?

  • Terminal control sequences (CSI ESC[, OSC ESC], ST ESC\) should never be interpreted as user input.
  • When a child process exits abnormally, kimi should wait for the ToolResult (with the error output and non-zero exit code) and display it normally.
  • The turn should continue until the LLM has processed the result, not be prematurely cancelled.

Suggested fix

Filter terminal escape sequences from the stdin input stream before passing them to the user-input parser:

# Pseudocode for the input sanitization layer
CSI_PATTERN   = re.compile(r'\x1b\[[0-?]*[ -/]*[@-~]')   # ESC [ ... letter
OSC_PATTERN   = re.compile(r'\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?')  # ESC ] ... ST
APC_PATTERN   = re.compile(r'\x1b_[^\x1b]*(?:\x1b\\)?')   # ESC _ ... ST

Or, more robustly: the input reader should only accept printable characters and explicit user actions (Enter, Ctrl+C, etc.) and drop any byte sequence that starts with ESC (0x1b) unless it is a known keybinding (e.g. arrow keys, which should be translated to their semantic meaning, not passed through as raw bytes).

Additional information

  • kimi-code version: 0.29.1 (binary release, ~/.kimi-code/bin/kimi)
  • Host OS: Ubuntu 22.04.5 LTS, glibc 2.35
  • Terminal: Ghostty on macOS (client), SSH to remote Linux host
  • SSH config: ServerAliveInterval 240, TCPKeepAlive yes
  • Workaround: Fix the underlying cause that makes the child crash (e.g. install a musl-linked binary instead of glibc-linked) so the TTY state change never happens.

Related but distinct issues

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