MoonshotAI / MoonshotAI/kimi-code

TUI memory leak: `!` shell output entries grow unbounded; folded entries are never reclaimed

Open
#2,556 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
7.5k
Forks
1.2k
Avg merge
11h 53m
Merged PRs (30d)
350

Description

TUI memory leak: ! shell output entries grow unbounded; folded entries are never reclaimed

Summary

The TUI's in-memory transcript entries can grow without bound in sessions that make heavy use of ! shell commands: entries with turnId: undefined group into a tail turn that is never trimmed, and each entry stores the full, untruncated command output — with a second full copy retained inside the ShellRunComponent, which is never folded either. Separately, foldCurrentTurnContent disposes folded components but never removes the corresponding entries from state.transcriptEntries.

Note: this issue was initially filed with an incorrect root-cause analysis (it claimed tool results were retained in transcriptEntries, and that goal mode kept a single turnId). Both were wrong; see the correction comment below. This body has been rewritten with verified facts.

Root cause (verified against HEAD 29c9e2a, 0.31.1)

All in apps/kimi-code/src/tui/:

  1. ! shell output entries are never trimmed.

    • Both the echo entry and the output entry are created with turnId: undefined (kimi-tui.ts:1060, kimi-tui.ts:1072).
    • groupTurns buffers undefined-turnId entries and folds leftovers at the tail into a single turn (utils/transcript-window.ts:74-97). That tail turn is the last turn, and turnsToTrim iterates for (let i = 0; i < turns.length - 1 ...) (transcript-window.ts:119) — it never touches the last turn.
    • entry.content holds the full, untruncated command output, and ShellRunComponent keeps its own full copy of finalStdout/finalStderr (the 256 KB cap only applies mid-stream, not to the final stored copy) — two unbounded copies per command.
    • These components are also never folded: the fold guard returns early when the merge batch contains no thinking/tool/assistant components (kimi-tui.ts:2246), so in a pure ! session the component tree itself is unbounded too.
  2. Folding never reclaims entries. foldCurrentTurnContent (kimi-tui.ts:2196) disposes folded components and splices the child list, but never removes the folded entries from state.transcriptEntries. The Component ↔ TranscriptEntry WeakMap (getTranscriptComponentEntry) already exists but is not used for reclamation. (The affected entries are assistant/status/! output entries. Tool results live in ToolCallComponent instances and are freed when folded components are disposed — the tool_call entry branch at kimi-tui.ts:1958 has no creation site and is dead code.)

  3. Normal sessions stay bounded. trimTranscriptWindow runs on every append (kimi-tui.ts:1995) and trims whole turns beyond maxTurns + hysteresis; goal mode allocates a fresh turnId per continuation (agent-core/src/agent/turn/index.ts:528), so long goal runs do fall out of the window. The unbounded paths are the current turn and the undefined tail turn.

Suggested fix

  • In foldCurrentTurnContent, collect folded entries via getTranscriptComponentEntry before the splice, then filter them out of state.transcriptEntries. Safe: session replay rebuilds via appendTranscriptEntry; undo anchors are boundary entries that are never folded.
  • For the undefined tail turn: either attach ! entries to the following turn, or let turnsToTrim cap an over-long tail turn; additionally truncate the ! output stored in entries/components (the streaming cap does not apply to the final stored copies).

Steps to reproduce

  1. In the TUI, run a long series of ! commands with large outputs (e.g. ! cat on big files in a loop).
  2. Watch process RSS: it grows monotonically and never comes back down; transcript entries and ShellRunComponent copies accumulate without bound.

Environment: kimi-code 0.31.1, verified against HEAD 29c9e2a (2026-08-03). Reproduction is platform-independent — the code paths are the same everywhere.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in apps/kimi-code/src/tui/kimi-tui.ts, especially foldCurrentTurnContent and the shell-entry paths around lines 1060, 1072, and 2196, then trace grouping and trimming in utils/transcript-window.ts. Reproduce the leak with repeated large-output ! commands and inspect transcriptEntries and ShellRunComponent copies. Done means folded entries and unbounded undefined-tail shell output are reclaimed or capped while normal transcript behavior remains bounded.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
cli, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.