What happened?
After running Qwen Code sessions for extended periods (dozens of hours, thousands of tokens), the process memory consumption continues to grow and never decreases.
Code analysis reveals the root cause: the UI History array (useHistoryManager.history) grows without bound, storing heavy objects (full file contents, terminal outputs, etc.) with no size limit, eviction policy, or garbage collection mechanism.
Root Cause Analysis
1. Primary Cause: Unbounded history array (Critical)
- File:
packages/cli/src/ui/hooks/useHistoryManager.ts
useState<HistoryItem[]>([]) continuously appends items with no maximum size limit, no eviction policy, and no pagination
- Only the
/clear command can clear it; during normal usage, it only ever grows
- Every user message, model response chunk, and tool call result is appended to this array
2. Heavy Objects Retained in Memory (Critical)
FileDiff stores originalContent and newContent with complete file contents, potentially MBs per tool call
AnsiOutputDisplay stores complete terminal output
- Once rendered to
<Static>, this data is never needed again but remains referenced by the history array, preventing GC
3. userMessages re-derived on every history change (High)
- File:
packages/cli/src/ui/AppContainer.tsx (lines 346-373)
useEffect depends on historyManager.history, triggered on every item append
- Internally executes filter → reverse → concat → dedup → reverse, O(n) complexity
- As history grows, each append creates many temporary objects, increasing GC pressure
4. Unbounded Caches (Medium)
- File:
packages/cli/src/ui/utils/textUtils.ts
codePointsCache (Map) and stringWidthCache (Map) have no eviction policy
clearStringWidthCache() function exists but is never called anywhere
5. Checkpoint Serialization Includes Full History (Medium)
- File:
packages/cli/src/ui/hooks/useGeminiStream.ts (lines 1466-1583)
- Every awaiting-approval tool call triggers
JSON.stringify of the entire history array
history is in the useEffect dependency, causing frequent triggers
6. Design Gap: Chat Compression Doesn't Affect UI History
chatCompressionService.ts only compresses the API-side Content history sent to the model
- The UI-side HistoryItem array remains completely untouched even after API-side compression
What did you expect to happen?
Memory usage during long sessions should remain bounded. Suggestions:
- Set a maximum size limit for the
history array (e.g., 5000 items). When exceeded, evict the oldest entries. (Items already rendered via <Static> won't visually disappear)
- Replace heavy fields like
FileDiff.originalContent/newContent and AnsiOutputDisplay with placeholders after rendering, releasing the original data for GC
- Add LRU eviction policy for
codePointsCache/stringWidthCache
- Decouple
userMessages derivation from history changes; update only when new user messages are actually added
- Checkpoint serialization should not include the full UI history
Related Files
| Issue |
Severity |
File |
Unbounded history array |
Critical |
useHistoryManager.ts |
| FileDiff stores full file contents |
Critical |
types.ts + tools.ts |
| AnsiOutputDisplay retained in memory |
High |
types.ts + tools.ts |
| userMessages O(n) re-derivation |
High |
AppContainer.tsx |
| Unbounded string caches |
Medium |
textUtils.ts |
| Checkpoint serializes full history |
Medium |
useGeminiStream.ts |
| No render virtualization/lazy loading |
Medium |
MainContent.tsx |
Client information
Observed across multiple sessions on macOS and Linux.
What happened?
After running Qwen Code sessions for extended periods (dozens of hours, thousands of tokens), the process memory consumption continues to grow and never decreases.
Code analysis reveals the root cause: the UI History array (
useHistoryManager.history) grows without bound, storing heavy objects (full file contents, terminal outputs, etc.) with no size limit, eviction policy, or garbage collection mechanism.Root Cause Analysis
1. Primary Cause: Unbounded
historyarray (Critical)packages/cli/src/ui/hooks/useHistoryManager.tsuseState<HistoryItem[]>([])continuously appends items with no maximum size limit, no eviction policy, and no pagination/clearcommand can clear it; during normal usage, it only ever grows2. Heavy Objects Retained in Memory (Critical)
FileDiffstoresoriginalContentandnewContentwith complete file contents, potentially MBs per tool callAnsiOutputDisplaystores complete terminal output<Static>, this data is never needed again but remains referenced by the history array, preventing GC3.
userMessagesre-derived on every history change (High)packages/cli/src/ui/AppContainer.tsx(lines 346-373)useEffectdepends onhistoryManager.history, triggered on every item append4. Unbounded Caches (Medium)
packages/cli/src/ui/utils/textUtils.tscodePointsCache(Map) andstringWidthCache(Map) have no eviction policyclearStringWidthCache()function exists but is never called anywhere5. Checkpoint Serialization Includes Full History (Medium)
packages/cli/src/ui/hooks/useGeminiStream.ts(lines 1466-1583)JSON.stringifyof the entirehistoryarrayhistoryis in the useEffect dependency, causing frequent triggers6. Design Gap: Chat Compression Doesn't Affect UI History
chatCompressionService.tsonly compresses the API-side Content history sent to the modelWhat did you expect to happen?
Memory usage during long sessions should remain bounded. Suggestions:
historyarray (e.g., 5000 items). When exceeded, evict the oldest entries. (Items already rendered via<Static>won't visually disappear)FileDiff.originalContent/newContentandAnsiOutputDisplaywith placeholders after rendering, releasing the original data for GCcodePointsCache/stringWidthCacheuserMessagesderivation from history changes; update only when new user messages are actually addedRelated Files
historyarrayuseHistoryManager.tstypes.ts+tools.tstypes.ts+tools.tsAppContainer.tsxtextUtils.tsuseGeminiStream.tsMainContent.tsxClient information
Observed across multiple sessions on macOS and Linux.