ContextManager: context window truncation strategy
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:
contextHistoryUpdatesstores 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
ContextManager class:44— Holds thecontextHistoryUpdatesmap; no singleton.initializeContextHistory:100— Reads historicalcontext_history_updatesfrom disk at startup.saveContextHistory:131— Persists in-memory updates back to the task directory.shouldCompactContextWindow:150— Boolean check for the main Task; compares the last request's token count to the threshold.getNewContextMessagesAndMetadata:227— Main entry point; decides whether to truncate and returns the truncated history and the new deletedRange.getNextTruncationRange:299— Given a keep strategy, computes the[start, end]range to delete.getTruncatedMessages:344— Public interface; applies history updates and returns the final message array to send.ensureToolResultsFollowToolUse:375— Fixes tool_use / tool_result pairing; deleting a chunk can leave them misaligned.truncateContextHistory:552— Deletes all updates after a given timestamp, for checkpoint rollback.applyContextOptimizations:606— Finds and saves file read optimization points.attemptFileReadOptimizationInMemory:696— In-memory version for SubagentRunner; returns the optimized conversation directly.getContextWindowInfo:10— ComputescontextWindowandmaxAllowedSize; different models get different buffers.
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:
// 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:
getAndAlterTruncatedMessagesreturns the original array directly (early return:358). - Equal deletedRange skips recompute:
compactConversationForContextWindowearly-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 -= 1steps 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/catchwrapsfs.writeFileand 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:
truncateContextHistorydoes 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.