Skip to content

ContextManager: context window truncation strategy

源码版本v4.0.10

Responsibilities

ContextManager is Cline's truncation and optimization handler for when "the context window is about to overflow". When the total tokens of a round of LLM requests approach the model's limit, the main Task or SubagentRunner calls its methods to delete a chunk of older conversation messages according to a strategy, while preserving the most recent few rounds, all tool definitions, and any earlier messages marked as "important". It does not store the original messages — those still live in the apiConversationHistory array — but maintains a conversationHistoryDeletedRange: [start, end] range that tells callers "this index range is invalid", plus an in-memory contextHistoryUpdates map that records overwriting modifications to message content.

In Cline's architecture it sits between "message history" and "the request body actually sent to the API". Before each round, the main Task uses getTruncatedMessages (getTruncatedMessages:344) to slice the history array into the version actually sent; after the request, if the token count is near the limit, it uses getNewContextMessagesAndMetadata (getNewContextMessagesAndMetadata:227) to decide whether to trim further. So ContextManager is a small mostly-functional module with some internal state, called by the main agent loop to decide "what to send on the next round".

Design motivation

  • Only record a range, never mutate the original array: maintaining [start, end] rather than actually splicing the array keeps checkpoint rollback cost at O(1) (getNextTruncationRange:299).
  • Always keep the first two: index 0 and 1 are the "core user/assistant pair"; after deletion the next message must still be an assistant message, preserving the user-assistant alternating structure (preserve pairing:331).
  • Four retention tiers: none / lastTwo / half / quarter, corresponding to how much is deleted. The more tokens exceed the limit, the less is kept (keep strategies:309).
  • Dynamic threshold: when totalTokens / 2 already exceeds maxAllowedSize, it jumps straight to quarter, covering the case of switching from a 200k model to a 64k model (quarter fallback:252).
  • File read optimization before truncation: large tool_result blocks from file reads are first compressed by "marking and restoring on demand", which can save over 30% and avoid truncation altogether (attemptFileReadOptimizationCore:626).
  • Timestamp ordering for rollback: contextHistoryUpdates stores entries as [timestamp, updateType, update] arrays; looking from right to left by time to find the cutoff allows precise reversal of modifications after a given point (truncate by timestamp:573).
  • auto-condense new path: next-gen models take the auto-condense path at a 0.75 threshold; older models still decide based on maxAllowedSize (thresholdPercentage:163).

Key files

Data flow

In the main Task flow, getNewContextMessagesAndMetadata is the key decision point. It reads the token count of the previous API request and compares it to maxAllowedSize; only if exceeded does it act:

typescript
// apps/vscode/src/core/context/context-management/ContextManager.ts
if (previousApiReqIndex >= 0) {
  const previousRequestText = clineMessages[previousApiReqIndex]?.text
  if (previousRequestText) {
    const timestamp = clineMessages[previousApiReqIndex].ts
    const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse(previousRequestText)
    const totalTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
    const { maxAllowedSize } = getContextWindowInfo(api)

    if (totalTokens >= maxAllowedSize) {
      // When switching models, half may not be enough, so if token/2 exceeds maxAllowedSize, jump to quarter
      const keep = totalTokens / 2 > maxAllowedSize ? "quarter" : "half"

      let { anyContextUpdates, needToTruncate } = this.attemptFileReadOptimizationCore(
        apiConversationHistory,
        conversationHistoryDeletedRange,
        timestamp,
      )

      if (needToTruncate) {
        anyContextUpdates = this.applyStandardContextTruncationNoticeChange(timestamp) || anyContextUpdates
        conversationHistoryDeletedRange = this.getNextTruncationRange(
          apiConversationHistory,
          conversationHistoryDeletedRange,
          keep,
        )
        updatedConversationHistoryDeletedRange = true
      }

      if (anyContextUpdates) {
        await this.saveContextHistory(taskDirectory)
      }
    }
  }
}

const truncatedConversationHistory = this.getAndAlterTruncatedMessages(
  apiConversationHistory,
  conversationHistoryDeletedRange,
)

This sits around main truncation logic:240. Once the truncation range is computed, getAndAlterTruncatedMessages walks the history after deletedRange[1] + 1 and runs applyContextHistoryUpdates (applyContextHistoryUpdates:362) to apply the overwriting modifications from file read optimization / standard truncation notices. ensureToolResultsFollowToolUse then does another pairing pass to ensure every tool_use is immediately followed by its tool_result.

Boundaries and failure modes

  • One or fewer messages is a no-op: getAndAlterTruncatedMessages returns the original array directly (early return:358).
  • Equal deletedRange skips recompute: compactConversationForContextWindow early-exits when the new range equals the old range (range equal early return:929), avoiding unnecessary rewrites.
  • Range end must be an assistant message: after deleting a chunk, the next message must be an assistant message; otherwise rangeEndIndex -= 1 steps back one (assistant pairing:333).
  • File read optimization below 30% still truncates: when percentSaved < 0.3, needToTruncate: true (30 percent threshold:654).
  • saveContextHistory disk write failure only logs: try/catch wraps fs.writeFile and does not re-throw into the main flow (save error handling:142).
  • Different models keep different buffers: when contextWindow <= 64k, an 80% formula is used to avoid thin buffers on small models (deepseek buffer:31).
  • Checkpoint rollback by timestamp: truncateContextHistory does not touch deletedRange; it only deletes updates after a given timestamp, allowing history to roll back to any checkpoint (truncateContextHistory:552).

Summary

ContextManager is the pillar that keeps Cline's long conversations from collapsing. To see how it is used by SubagentRunner, read subagent; to see how the main Task calls it recursively to decide the next round's history, read agent-loop/task-class; for task directory and persistence, read storage/state-manager.

See official docs: Cline 文档 · README