ContextManager:上下文窗口截断策略
职责
ContextManager 是 Cline 在「上下文 (context) 窗口快要撑爆」时的截断与优化器。当一轮 LLM 请求的 total token 接近模型上限,主 Task 或 SubagentRunner 会调它的方法,把老的对话消息按策略删掉一段,同时保留最新的几轮、所有工具定义、以及被标为「重要」的更早消息。它不存原始消息——那还在 apiConversationHistory 数组里——而是维护一个 conversationHistoryDeletedRange: [start, end] 区间,告诉调用方「这段索引已经无效」,外加一个 contextHistoryUpdates 的 in-memory map 记录对消息内容的覆盖式修改。
它在 Cline 架构里位置介于「消息历史」和「实际发往 API 的请求体」之间。每轮请求前,主 Task 用 getTruncatedMessages (getTruncatedMessages:344) 把 history 数组切成实际下发的版本;请求完发现 token 数临界,再用 getNewContextMessagesAndMetadata (getNewContextMessagesAndMetadata:227) 决定要不要进一步删。所以 ContextManager 是个纯函数式 + 内部状态的小模块,主 agent loop 调它来决定「下一轮发什么」。
设计动机
- 只删区间不改原数组:维护
[start, end]而不是真去 splice 数组,让 checkpoint 回滚成本 O(1) (getNextTruncationRange:299)。 - 永远留前两条:index 0 和 1 是「核心 user/assistant pair」,删完之后下一条仍必须是 assistant 消息,保持 user-assistant 交替结构 (
preserve pairing:331)。 - 四档保留策略:
none/lastTwo/half/quarter,对应删多少。token 超得越多保留越少 (keep strategies:309)。 - 动态阈值:totalTokens / 2 已经大于 maxAllowedSize 时直接走
quarter,覆盖从 200k 模型切到 64k 模型的场景 (quarter fallback:252)。 - file read 优化先于截断:先把读文件的大块 tool_result 用「标记 + 后续按需还原」的方式压缩,能省 30% 以上就免截断 (
attemptFileReadOptimizationCore:626)。 - timestamp 排序支持回滚:
contextHistoryUpdates用[timestamp, updateType, update]数组存,按时间从右往左找 cutoff,能精确撤销某时间点之后的修改 (truncate by timestamp:573)。 - auto-condense 新路径:next-gen 模型走 0.75 阈值的 auto-condense,旧模型仍按
maxAllowedSize判断 (thresholdPercentage:163)。
关键文件
ContextManager class:44— 持有contextHistoryUpdatesmap,无单例。initializeContextHistory:100— 启动时从磁盘读历史context_history_updates。saveContextHistory:131— 把内存里的 updates 持久化回 task 目录。shouldCompactContextWindow:150— 给主 Task 用的布尔判断,按上次请求 token 数对阈值。getNewContextMessagesAndMetadata:227— 主入口,决定是否截断、返回截断后的 history 和新的 deletedRange。getNextTruncationRange:299— 给定 keep 策略,算出要删的[start, end]区间。getTruncatedMessages:344— 公开接口,应用 history updates 后返回最终下发的消息数组。ensureToolResultsFollowToolUse:375— 校正 tool_use 和 tool_result 的配对,删一段后可能错位。truncateContextHistory:552— 删除给定 timestamp 之后的所有 updates,用于 checkpoint 回滚。applyContextOptimizations:606— 找并保存 file read 优化点。attemptFileReadOptimizationInMemory:696— 给 SubagentRunner 用的内存版本,直接返回优化后的 conversation。getContextWindowInfo:10— 把contextWindow和maxAllowedSize算出来,不同模型留不同 buffer。
数据流
主 Task 流程下,getNewContextMessagesAndMetadata 是关键决策点。进来先读上一次 API 请求的 token 数,跟 maxAllowedSize 比,超了才动:
// 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) {
// 切模型时 half 可能不够,所以 token/2 超 maxAllowedSize 直接走 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,
)这段在 main truncation logic:240 附近。截断区间算出来后,getAndAlterTruncatedMessages 会把 deletedRange[1] + 1 之后的 history 跑一遍 applyContextHistoryUpdates (applyContextHistoryUpdates:362),把 file read 优化 / 标准截断提示等覆盖式修改 apply 上去。ensureToolResultsFollowToolUse 再做一次配对校正,保证每个 tool_use 后面紧跟它的 tool_result。
边界与失败
- 消息少于等于 1 条不处理:
getAndAlterTruncatedMessages直接返回原数组 (early return:358)。 - deletedRange 相同时不重算:
compactConversationForContextWindow检测 new range 跟 old range 完全相等就早退 (range equal early return:929),避免无谓重写。 - range 结尾必须是 assistant:删完一段后下一条必须是 assistant 消息,否则
rangeEndIndex -= 1回退一位 (assistant pairing:333)。 - file read 优化省不够 30% 仍要截断:
percentSaved < 0.3时needToTruncate: true(30 percent threshold:654)。 - saveContextHistory 写盘失败只记 log:
try/catch包住fs.writeFile,不抛回主流程 (save error handling:142)。 - 不同模型留不同 buffer:
contextWindow <= 64k走 80% 公式,避免小模型 buffer 太薄 (deepseek buffer:31)。 - checkpoint 回滚走时间戳:
truncateContextHistory不改 deletedRange,只删指定时间戳之后的 update,让历史能回退到任意 checkpoint (truncateContextHistory:552)。
小结
ContextManager 是 Cline 长会话不崩的支柱。想看它怎么被 SubagentRunner 用可以读 subagent;想看主 Task 如何在递归里调它决定下一轮的 history 可以读 agent-loop/task-class;想看 task 目录和持久化相关可以读 storage/state-manager。