ContextManager:コンテキストウィンドウの切り詰め戦略
役割
ContextManager は Cline が「コンテキストウィンドウ (context window) が限界に達しそうな時」の切り詰めと最適化を担う。1 ラウンドの 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 が「次ラウンドで何を送るか」を決めるために呼ぶ。
設計動機
- 区間だけ削り元配列は変更しない:配列を splice する代わりに
[start, end]を管理することで、checkpoint ロールバックのコストを O(1) にする (getNextTruncationRange:299)。 - 前 2 条を必ず保持:index 0 と 1 は「中核 user/assistant pair」。削除後も次のメッセージは assistant メッセージでなければならず、user-assistant 交互構造を保つ (
preserve pairing:331)。 - 4 段階の保持戦略:
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 用の boolean 判定。前回リクエストの token 数をしきい値と比較。getNewContextMessagesAndMetadata:227— 主エントリ。切り詰め要否を判断し、切り詰め後の history と新しい deletedRange を返す。getNextTruncationRange:299— keep 戦略を与えると、削る[start, end]区間を算出。getTruncatedMessages:344— 公開 API。history updates を適用後、最終的に送信するメッセージ配列を返す。ensureToolResultsFollowToolUse:375— tool_use と tool_result のペアを補正。1 段削除した後でずれが生じうるため。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 最適化 / 標準切り詰め通知などの上書き修正を適用する。さらに ensureToolResultsFollowToolUse がペア補正を行い、各 tool_use の直後に tool_result が続くことを保証する。
境界と失敗
- メッセージが 1 条以下は非処理:
getAndAlterTruncatedMessagesは元の配列をそのまま返す (early return:358)。 - deletedRange が同じ時は再計算しない:
compactConversationForContextWindowは new range と old range が完全一致すれば早期リターン (range equal early return:929)。無駄な書き換えを避ける。 - range 終端は assistant でなければならない:1 段削除後、次のメッセージが assistant でなければ
rangeEndIndex -= 1で 1 つ戻す (assistant pairing:333)。 - file read 最適化が 30% 未満でも切り詰め:
percentSaved < 0.3ならneedToTruncate: true(30 percent threshold:654)。 - saveContextHistory のディスク書き込み失敗は log のみ:
fs.writeFileを try/catch で囲み、主フローに例外を伝播させない (save error handling:142)。 - モデルごとに buffer を変える:
contextWindow <= 64kは 80% 公式を使い、小モデルの buffer が薄くなりすぎるのを回避 (deepseek buffer:31)。 - checkpoint ロールバックはタイムスタンプ基準:
truncateContextHistoryは deletedRange を変更せず、指定 timestamp 以降の update のみ削除する。履歴を任意 checkpoint まで戻せる (truncateContextHistory:552)。
まとめ
ContextManager は Cline が長会話でも崩れない柱である。SubagentRunner がどう使うかは subagent、主 Task が再帰の中でどう呼び出して次ラウンドの history を決めるかは agent-loop/task-class、task ディレクトリと永続化周りは storage/state-manager を参照のこと。