Skip to content

Memory grows unboundedly during long sessions — UI History accumulates without limit #2128

@yiliang114

Description

@yiliang114

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:

  1. 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)
  2. Replace heavy fields like FileDiff.originalContent/newContent and AnsiOutputDisplay with placeholders after rendering, releasing the original data for GC
  3. Add LRU eviction policy for codePointsCache/stringWidthCache
  4. Decouple userMessages derivation from history changes; update only when new user messages are actually added
  5. 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.

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions