MoonshotAI / MoonshotAI/kimi-code
TUI memory leak: `!` shell output entries grow unbounded; folded entries are never reclaimed
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/:
-
!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). groupTurnsbuffers 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, andturnsToTrimiteratesfor (let i = 0; i < turns.length - 1 ...)(transcript-window.ts:119) — it never touches the last turn.entry.contentholds the full, untruncated command output, andShellRunComponentkeeps its own full copy offinalStdout/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.
- Both the echo entry and the output entry are created with
-
Folding never reclaims entries.
foldCurrentTurnContent(kimi-tui.ts:2196) disposes folded components and splices the child list, but never removes the folded entries fromstate.transcriptEntries. TheComponent ↔ TranscriptEntryWeakMap (getTranscriptComponentEntry) already exists but is not used for reclamation. (The affected entries are assistant/status/!output entries. Tool results live inToolCallComponentinstances and are freed when folded components are disposed — thetool_callentry branch atkimi-tui.ts:1958has no creation site and is dead code.) -
Normal sessions stay bounded.
trimTranscriptWindowruns on every append (kimi-tui.ts:1995) and trims whole turns beyondmaxTurns + 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 viagetTranscriptComponentEntrybefore the splice, then filter them out ofstate.transcriptEntries. Safe: session replay rebuilds viaappendTranscriptEntry; undo anchors are boundary entries that are never folded. - For the undefined tail turn: either attach
!entries to the following turn, or letturnsToTrimcap 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
- In the TUI, run a long series of
!commands with large outputs (e.g.! caton big files in a loop). - Watch process RSS: it grows monotonically and never comes back down; transcript entries and
ShellRunComponentcopies 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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