mirror of
https://github.com/cline/cline.git
synced 2026-09-11 16:42:40 +08:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d67cfdac78 | ||
|
|
cf1bc8e7a7 | ||
|
|
4be36bca63 | ||
|
|
43bb1f2c97 |
+13
-2
@@ -75,6 +75,12 @@ message TaskCompleteData {
|
||||
map<string, string> task_metadata = 1;
|
||||
}
|
||||
|
||||
// Message for deleted range tuple (start, end indices)
|
||||
message DeletedRange {
|
||||
int32 start_index = 1;
|
||||
int32 end_index = 2;
|
||||
}
|
||||
|
||||
// Data for PreCompact hook
|
||||
message PreCompactData {
|
||||
// Task identification
|
||||
@@ -100,9 +106,11 @@ message PreCompactData {
|
||||
int64 tokens_in_cache = 8;
|
||||
int64 tokens_out_cache = 9;
|
||||
|
||||
// DEPRECATED
|
||||
// Truncation information (if applicable)
|
||||
int32 deleted_range_start = 10; // Start index of deleted conversation range
|
||||
int32 deleted_range_end = 11; // End index of deleted conversation range
|
||||
// int32 deleted_range_start = 10; // Start index of deleted conversation range
|
||||
// int32 deleted_range_end = 11; // End index of deleted conversation range
|
||||
reserved 10,11;
|
||||
|
||||
// Context JSON file path
|
||||
// Path to a temporary JSON file containing the full API conversation history
|
||||
@@ -118,4 +126,7 @@ message PreCompactData {
|
||||
// Use this to analyze total context size, overhead, and exactly what the model sees
|
||||
// This file will be automatically cleaned up after the hook completes
|
||||
string context_raw_path = 13;
|
||||
|
||||
// Multiple non-contiguous deleted ranges (for advanced context compaction)
|
||||
repeated DeletedRange deleted_ranges = 14;
|
||||
}
|
||||
|
||||
@@ -218,6 +218,8 @@ message ClineMessage {
|
||||
ClineAskNewTask ask_new_task = 21;
|
||||
ClineApiReqInfo api_req_info = 22;
|
||||
ClineModelInfo model_info = 23;
|
||||
|
||||
repeated ConversationHistoryDeletedRange conversation_history_deleted_ranges = 24;
|
||||
}
|
||||
|
||||
// UiService provides methods for managing UI interactions
|
||||
|
||||
@@ -189,6 +189,7 @@ async function reconstructTaskHistoryItem(taskId: string): Promise<HistoryItem |
|
||||
size: taskInfo.size,
|
||||
isFavorited: taskInfo.isFavorited,
|
||||
conversationHistoryDeletedRange: taskInfo.conversationHistoryDeletedRange,
|
||||
conversationHistoryDeletedRanges: taskInfo.conversationHistoryDeletedRanges,
|
||||
}
|
||||
|
||||
return historyItem
|
||||
@@ -209,6 +210,7 @@ interface TaskInfo {
|
||||
size?: number
|
||||
isFavorited?: boolean
|
||||
conversationHistoryDeletedRange?: [number, number]
|
||||
conversationHistoryDeletedRanges?: Array<[number, number]>
|
||||
}
|
||||
|
||||
function extractTaskInformation(clineMessages: ClineMessage[], metadata: any): TaskInfo {
|
||||
|
||||
@@ -221,12 +221,12 @@ export class ContextManager {
|
||||
apiConversationHistory: Anthropic.Messages.MessageParam[],
|
||||
clineMessages: ClineMessage[],
|
||||
api: ApiHandler,
|
||||
conversationHistoryDeletedRange: [number, number] | undefined,
|
||||
conversationHistoryDeletedRanges: Array<[number, number]> | undefined,
|
||||
previousApiReqIndex: number,
|
||||
taskDirectory: string,
|
||||
useAutoCondense: boolean, // option to use new auto-condense or old programmatic context management
|
||||
) {
|
||||
let updatedConversationHistoryDeletedRange = false
|
||||
let updatedConversationHistoryDeletedRanges = false
|
||||
|
||||
if (!useAutoCondense) {
|
||||
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
|
||||
@@ -247,7 +247,7 @@ export class ContextManager {
|
||||
// Attempt file read optimization and check if we need to truncate
|
||||
let { anyContextUpdates, needToTruncate } = this.attemptFileReadOptimizationCore(
|
||||
apiConversationHistory,
|
||||
conversationHistoryDeletedRange,
|
||||
conversationHistoryDeletedRanges,
|
||||
timestamp,
|
||||
)
|
||||
|
||||
@@ -256,13 +256,13 @@ export class ContextManager {
|
||||
anyContextUpdates = this.applyStandardContextTruncationNoticeChange(timestamp) || anyContextUpdates
|
||||
|
||||
// NOTE: it's okay that we overwriteConversationHistory in resume task since we're only ever removing the last user message and not anything in the middle which would affect this range
|
||||
conversationHistoryDeletedRange = this.getNextTruncationRange(
|
||||
conversationHistoryDeletedRanges = this.getNextTruncationRange(
|
||||
apiConversationHistory,
|
||||
conversationHistoryDeletedRange,
|
||||
conversationHistoryDeletedRanges,
|
||||
keep,
|
||||
)
|
||||
|
||||
updatedConversationHistoryDeletedRange = true
|
||||
updatedConversationHistoryDeletedRanges = true
|
||||
}
|
||||
|
||||
// if we alter the context history, save the updated version to disk
|
||||
@@ -276,12 +276,12 @@ export class ContextManager {
|
||||
|
||||
const truncatedConversationHistory = this.getAndAlterTruncatedMessages(
|
||||
apiConversationHistory,
|
||||
conversationHistoryDeletedRange,
|
||||
conversationHistoryDeletedRanges,
|
||||
)
|
||||
|
||||
return {
|
||||
conversationHistoryDeletedRange: conversationHistoryDeletedRange,
|
||||
updatedConversationHistoryDeletedRange: updatedConversationHistoryDeletedRange,
|
||||
conversationHistoryDeletedRanges: conversationHistoryDeletedRanges,
|
||||
updatedConversationHistoryDeletedRanges: updatedConversationHistoryDeletedRanges,
|
||||
truncatedConversationHistory: truncatedConversationHistory,
|
||||
}
|
||||
}
|
||||
@@ -291,12 +291,15 @@ export class ContextManager {
|
||||
*/
|
||||
public getNextTruncationRange(
|
||||
apiMessages: Anthropic.Messages.MessageParam[],
|
||||
currentDeletedRange: [number, number] | undefined,
|
||||
currentDeletedRanges: Array<[number, number]> | undefined,
|
||||
keep: "none" | "lastTwo" | "half" | "quarter",
|
||||
): [number, number] {
|
||||
): Array<[number, number]> {
|
||||
// Extract current range - undefined if no truncation yet
|
||||
currentDeletedRanges = currentDeletedRanges && currentDeletedRanges.length > 0 ? currentDeletedRanges : undefined
|
||||
|
||||
// We always keep the first user-assistant pairing, and truncate an even number of messages from there
|
||||
const rangeStartIndex = 2 // index 0 and 1 are kept
|
||||
const startOfRest = currentDeletedRange ? currentDeletedRange[1] + 1 : 2 // inclusive starting index
|
||||
const startOfRest = currentDeletedRanges ? currentDeletedRanges[0][1] + 1 : 2 // inclusive starting index
|
||||
|
||||
let messagesToRemove: number
|
||||
if (keep === "none") {
|
||||
@@ -328,7 +331,7 @@ export class ContextManager {
|
||||
}
|
||||
|
||||
// this is an inclusive range that will be removed from the conversation history
|
||||
return [rangeStartIndex, rangeEndIndex]
|
||||
return [[rangeStartIndex, rangeEndIndex]]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -336,9 +339,9 @@ export class ContextManager {
|
||||
*/
|
||||
public getTruncatedMessages(
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
deletedRange: [number, number] | undefined,
|
||||
deletedRanges: Array<[number, number]> | undefined,
|
||||
): Anthropic.Messages.MessageParam[] {
|
||||
return this.getAndAlterTruncatedMessages(messages, deletedRange)
|
||||
return this.getAndAlterTruncatedMessages(messages, deletedRanges)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -346,13 +349,16 @@ export class ContextManager {
|
||||
*/
|
||||
private getAndAlterTruncatedMessages(
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
deletedRange: [number, number] | undefined,
|
||||
deletedRanges: Array<[number, number]> | undefined,
|
||||
): Anthropic.Messages.MessageParam[] {
|
||||
if (messages.length <= 1) {
|
||||
return messages
|
||||
}
|
||||
|
||||
const updatedMessages = this.applyContextHistoryUpdates(messages, deletedRange ? deletedRange[1] + 1 : 2)
|
||||
const updatedMessages = this.applyContextHistoryUpdates(
|
||||
messages,
|
||||
deletedRanges && deletedRanges.length > 0 ? deletedRanges[0][1] + 1 : 2,
|
||||
)
|
||||
|
||||
// Validate and fix tool_use/tool_result pairing
|
||||
this.ensureToolResultsFollowToolUse(updatedMessages)
|
||||
@@ -618,13 +624,16 @@ export class ContextManager {
|
||||
*/
|
||||
private attemptFileReadOptimizationCore(
|
||||
apiConversationHistory: Anthropic.Messages.MessageParam[],
|
||||
conversationHistoryDeletedRange: [number, number] | undefined,
|
||||
conversationHistoryDeletedRanges: Array<[number, number]> | undefined,
|
||||
timestamp: number,
|
||||
): {
|
||||
anyContextUpdates: boolean
|
||||
needToTruncate: boolean
|
||||
} {
|
||||
const startIndex = conversationHistoryDeletedRange ? conversationHistoryDeletedRange[1] + 1 : 2
|
||||
const startIndex =
|
||||
conversationHistoryDeletedRanges && conversationHistoryDeletedRanges.length > 0
|
||||
? conversationHistoryDeletedRanges[0][1] + 1
|
||||
: 2
|
||||
|
||||
const [anyContextUpdates, uniqueFileReadIndices] = this.applyContextOptimizations(
|
||||
apiConversationHistory,
|
||||
@@ -638,7 +647,7 @@ export class ContextManager {
|
||||
|
||||
const percentSaved = this.calculateContextOptimizationMetrics(
|
||||
apiConversationHistory,
|
||||
conversationHistoryDeletedRange,
|
||||
conversationHistoryDeletedRanges,
|
||||
uniqueFileReadIndices,
|
||||
)
|
||||
|
||||
@@ -653,7 +662,7 @@ export class ContextManager {
|
||||
*/
|
||||
async attemptFileReadOptimization(
|
||||
apiConversationHistory: Anthropic.Messages.MessageParam[],
|
||||
conversationHistoryDeletedRange: [number, number] | undefined,
|
||||
conversationHistoryDeletedRanges: Array<[number, number]> | undefined,
|
||||
clineMessages: ClineMessage[],
|
||||
previousApiReqIndex: number,
|
||||
taskDirectory: string,
|
||||
@@ -672,7 +681,7 @@ export class ContextManager {
|
||||
|
||||
const { anyContextUpdates, needToTruncate } = this.attemptFileReadOptimizationCore(
|
||||
apiConversationHistory,
|
||||
conversationHistoryDeletedRange,
|
||||
conversationHistoryDeletedRanges,
|
||||
timestamp,
|
||||
)
|
||||
|
||||
@@ -1232,7 +1241,7 @@ export class ContextManager {
|
||||
*/
|
||||
private calculateContextOptimizationMetrics(
|
||||
apiMessages: Anthropic.Messages.MessageParam[],
|
||||
conversationHistoryDeletedRange: [number, number] | undefined,
|
||||
conversationHistoryDeletedRanges: Array<[number, number]> | undefined,
|
||||
uniqueFileReadIndices: Set<number>,
|
||||
): number {
|
||||
// count for first user-assistant message pair
|
||||
@@ -1241,7 +1250,9 @@ export class ContextManager {
|
||||
// count for the remaining in-range messages
|
||||
const secondChunkResult = this.countCharactersAndSavingsInRange(
|
||||
apiMessages,
|
||||
conversationHistoryDeletedRange ? conversationHistoryDeletedRange[1] + 1 : 2,
|
||||
conversationHistoryDeletedRanges && conversationHistoryDeletedRanges.length > 0
|
||||
? conversationHistoryDeletedRanges[0][1] + 1
|
||||
: 2,
|
||||
apiMessages.length,
|
||||
uniqueFileReadIndices,
|
||||
)
|
||||
|
||||
@@ -34,24 +34,24 @@ describe("ContextManager", () => {
|
||||
const messages = createMessages(11)
|
||||
const result = contextManager.getNextTruncationRange(messages, undefined, "half")
|
||||
|
||||
expect(result).to.deep.equal([2, 5])
|
||||
expect(result).to.deep.equal([[2, 5]])
|
||||
})
|
||||
|
||||
it("first truncation with quarter keep", () => {
|
||||
const messages = createMessages(11)
|
||||
const result = contextManager.getNextTruncationRange(messages, undefined, "quarter")
|
||||
|
||||
expect(result).to.deep.equal([2, 7])
|
||||
expect(result).to.deep.equal([[2, 7]])
|
||||
})
|
||||
|
||||
it("sequential truncation with half keep", () => {
|
||||
const messages = createMessages(21)
|
||||
const firstRange = contextManager.getNextTruncationRange(messages, undefined, "half")
|
||||
expect(firstRange).to.deep.equal([2, 9])
|
||||
expect(firstRange).to.deep.equal([[2, 9]])
|
||||
|
||||
// Pass the previous range for sequential truncation
|
||||
const secondRange = contextManager.getNextTruncationRange(messages, firstRange, "half")
|
||||
expect(secondRange).to.deep.equal([2, 13])
|
||||
expect(secondRange).to.deep.equal([[2, 13]])
|
||||
})
|
||||
|
||||
it("sequential truncation with quarter keep", () => {
|
||||
@@ -60,8 +60,8 @@ describe("ContextManager", () => {
|
||||
|
||||
const secondRange = contextManager.getNextTruncationRange(messages, firstRange, "quarter")
|
||||
|
||||
expect(secondRange[0]).to.equal(2)
|
||||
expect(secondRange[1]).to.be.greaterThan(firstRange[1])
|
||||
expect(secondRange[0][0]).to.equal(2)
|
||||
expect(secondRange[0][1]).to.be.greaterThan(firstRange[0][1])
|
||||
})
|
||||
|
||||
it("ensures the last message in range is a user message", () => {
|
||||
@@ -69,11 +69,11 @@ describe("ContextManager", () => {
|
||||
const result = contextManager.getNextTruncationRange(messages, undefined, "half")
|
||||
|
||||
// Check if the message at the end of range is an assistant message
|
||||
const lastRemovedMessage = messages[result[1]]
|
||||
const lastRemovedMessage = messages[result[0][1]]
|
||||
expect(lastRemovedMessage.role).to.equal("assistant")
|
||||
|
||||
// Check if the next message after the range is a user message
|
||||
const nextMessage = messages[result[1] + 1]
|
||||
const nextMessage = messages[result[0][1] + 1]
|
||||
expect(nextMessage.role).to.equal("user")
|
||||
})
|
||||
|
||||
@@ -81,7 +81,7 @@ describe("ContextManager", () => {
|
||||
const messages = createMessages(3)
|
||||
const result = contextManager.getNextTruncationRange(messages, undefined, "half")
|
||||
|
||||
expect(result).to.deep.equal([2, 1])
|
||||
expect(result).to.deep.equal([[2, 1]])
|
||||
})
|
||||
|
||||
it("preserves the message structure when truncating", () => {
|
||||
@@ -89,7 +89,7 @@ describe("ContextManager", () => {
|
||||
const result = contextManager.getNextTruncationRange(messages, undefined, "half")
|
||||
|
||||
// Get messages after removing the range
|
||||
const effectiveMessages = [...messages.slice(0, result[0]), ...messages.slice(result[1] + 1)]
|
||||
const effectiveMessages = [...messages.slice(0, result[0][0]), ...messages.slice(result[0][1] + 1)]
|
||||
|
||||
// Check first message and alternating pattern
|
||||
expect(effectiveMessages[0].role).to.equal("user")
|
||||
@@ -302,7 +302,7 @@ describe("ContextManager", () => {
|
||||
const messages = createMessages(5)
|
||||
|
||||
const range: [number, number] = [1, 3]
|
||||
const result = contextManager.getTruncatedMessages(messages, range)
|
||||
const result = contextManager.getTruncatedMessages(messages, [range])
|
||||
|
||||
expect(result).to.have.lengthOf(3)
|
||||
expect(result[0]).to.deep.equal(messages[0])
|
||||
@@ -314,7 +314,7 @@ describe("ContextManager", () => {
|
||||
const messages = createMessages(4)
|
||||
|
||||
const range: [number, number] = [1, 2]
|
||||
const result = contextManager.getTruncatedMessages(messages, range)
|
||||
const result = contextManager.getTruncatedMessages(messages, [range])
|
||||
|
||||
expect(result).to.have.lengthOf(3)
|
||||
expect(result[0]).to.deep.equal(messages[0])
|
||||
@@ -326,7 +326,7 @@ describe("ContextManager", () => {
|
||||
const messages = createMessages(5)
|
||||
|
||||
const range: [number, number] = [2, 3]
|
||||
const result = contextManager.getTruncatedMessages(messages, range)
|
||||
const result = contextManager.getTruncatedMessages(messages, [range])
|
||||
|
||||
expect(result).to.have.lengthOf(3)
|
||||
expect(result[0]).to.deep.equal(messages[0])
|
||||
@@ -364,7 +364,7 @@ describe("ContextManager", () => {
|
||||
|
||||
// Truncate to remove the assistant message with tool_use
|
||||
const range: [number, number] = [2, 2]
|
||||
const result = contextManager.getTruncatedMessages(messages, range)
|
||||
const result = contextManager.getTruncatedMessages(messages, [range])
|
||||
|
||||
// Should have 4 messages (original 5 minus 1 truncated)
|
||||
expect(result).to.have.lengthOf(4)
|
||||
|
||||
@@ -124,6 +124,8 @@ export interface PreCompactHookParams {
|
||||
apiConversationHistory: ClineStorageMessage[]
|
||||
/** Current deleted range (if any) */
|
||||
conversationHistoryDeletedRange?: [number, number]
|
||||
/** Multiple non-contiguous deleted ranges (if any) */
|
||||
conversationHistoryDeletedRanges?: Array<[number, number]>
|
||||
/** Cline messages for extracting token usage */
|
||||
clineMessages: ClineMessage[]
|
||||
|
||||
@@ -136,8 +138,8 @@ export interface PreCompactHookParams {
|
||||
// Compaction metadata
|
||||
/** Compaction strategy to report in hook data */
|
||||
compactionStrategy: string
|
||||
/** Optional: Pre-calculated deleted range to report */
|
||||
deletedRange?: [number, number]
|
||||
/** Optional: Pre-calculated deleted ranges to report */
|
||||
deletedRanges?: Array<[number, number]>
|
||||
|
||||
// UI callbacks
|
||||
/** Callback to display messages */
|
||||
@@ -191,7 +193,7 @@ export async function executePreCompactHookWithCleanup(params: PreCompactHookPar
|
||||
// Get current active context (respects previous compactions)
|
||||
const currentContext = params.contextManager.getTruncatedMessages(
|
||||
params.apiConversationHistory,
|
||||
params.conversationHistoryDeletedRange,
|
||||
params.conversationHistoryDeletedRanges,
|
||||
)
|
||||
|
||||
// Write context files for hook access
|
||||
@@ -204,13 +206,11 @@ export async function executePreCompactHookWithCleanup(params: PreCompactHookPar
|
||||
const previousRequest = previousApiReqIndex !== -1 ? params.clineMessages[previousApiReqIndex] : undefined
|
||||
const { tokensIn, tokensOut, tokensInCache, tokensOutCache } = extractTokenUsageFromMessage(previousRequest)
|
||||
|
||||
// Extract truncation range - use provided range or extract from conversationHistoryDeletedRange
|
||||
let deletedRangeStart = 0
|
||||
let deletedRangeEnd = 0
|
||||
if (params.deletedRange) {
|
||||
;[deletedRangeStart, deletedRangeEnd] = params.deletedRange
|
||||
} else if (params.conversationHistoryDeletedRange) {
|
||||
;[deletedRangeStart, deletedRangeEnd] = params.conversationHistoryDeletedRange
|
||||
let deletedRanges: Array<[number, number]> = []
|
||||
if (params.deletedRanges && params.deletedRanges.length > 0) {
|
||||
deletedRanges = params.deletedRanges
|
||||
} else if (params.conversationHistoryDeletedRanges && params.conversationHistoryDeletedRanges.length > 0) {
|
||||
deletedRanges = params.conversationHistoryDeletedRanges
|
||||
}
|
||||
|
||||
// Execute the hook
|
||||
@@ -227,10 +227,12 @@ export async function executePreCompactHookWithCleanup(params: PreCompactHookPar
|
||||
tokensOut,
|
||||
tokensInCache,
|
||||
tokensOutCache,
|
||||
deletedRangeStart,
|
||||
deletedRangeEnd,
|
||||
contextJsonPath: contextJsonPath,
|
||||
contextRawPath: contextRawPath,
|
||||
deletedRanges: deletedRanges.map((range) => ({
|
||||
startIndex: range[0],
|
||||
endIndex: range[1],
|
||||
})),
|
||||
},
|
||||
},
|
||||
isCancellable: true,
|
||||
|
||||
@@ -34,6 +34,7 @@ export class TaskState {
|
||||
|
||||
// Context and history
|
||||
conversationHistoryDeletedRange?: [number, number]
|
||||
conversationHistoryDeletedRanges?: Array<[number, number]>
|
||||
|
||||
// Tool execution flags
|
||||
didRejectTool = false
|
||||
|
||||
+46
-24
@@ -322,6 +322,16 @@ export class Task {
|
||||
this.ulid = historyItem.ulid ?? ulid()
|
||||
this.taskIsFavorited = historyItem.isFavorited
|
||||
this.taskState.conversationHistoryDeletedRange = historyItem.conversationHistoryDeletedRange
|
||||
// Migration: if conversationHistoryDeletedRange exists but conversationHistoryDeletedRanges doesn't, convert
|
||||
if (
|
||||
historyItem.conversationHistoryDeletedRange !== undefined &&
|
||||
historyItem.conversationHistoryDeletedRanges === undefined
|
||||
) {
|
||||
this.taskState.conversationHistoryDeletedRanges = [historyItem.conversationHistoryDeletedRange]
|
||||
} else {
|
||||
// Going forward we will no longer keep conversationHistoryDeletedRange up to date as it will not be used
|
||||
this.taskState.conversationHistoryDeletedRanges = historyItem.conversationHistoryDeletedRanges
|
||||
}
|
||||
if (historyItem.checkpointManagerErrorMessage) {
|
||||
this.taskState.checkpointManagerErrorMessage = historyItem.checkpointManagerErrorMessage
|
||||
}
|
||||
@@ -382,6 +392,7 @@ export class Task {
|
||||
cancelTask: this.cancelTask,
|
||||
postStateToWebview: this.postStateToWebview,
|
||||
initialConversationHistoryDeletedRange: this.taskState.conversationHistoryDeletedRange,
|
||||
initialConversationHistoryDeletedRanges: this.taskState.conversationHistoryDeletedRanges,
|
||||
initialCheckpointManagerErrorMessage: this.taskState.checkpointManagerErrorMessage,
|
||||
stateManager: this.stateManager,
|
||||
})
|
||||
@@ -868,18 +879,18 @@ export class Task {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the new deleted range for PreCompact hook
|
||||
* Calculate the new deleted ranges for PreCompact hook
|
||||
* @param apiConversationHistory The full API conversation history
|
||||
* @returns Tuple with start and end indices for the deleted range
|
||||
* @returns Array of deleted ranges
|
||||
*/
|
||||
private calculatePreCompactDeletedRange(apiConversationHistory: ClineStorageMessage[]): [number, number] {
|
||||
const newDeletedRange = this.contextManager.getNextTruncationRange(
|
||||
private calculatePreCompactDeletedRanges(apiConversationHistory: ClineStorageMessage[]): Array<[number, number]> {
|
||||
const newDeletedRanges = this.contextManager.getNextTruncationRange(
|
||||
apiConversationHistory,
|
||||
this.taskState.conversationHistoryDeletedRange,
|
||||
this.taskState.conversationHistoryDeletedRanges,
|
||||
"quarter", // Force aggressive truncation on error
|
||||
)
|
||||
|
||||
return newDeletedRange || [0, 0]
|
||||
return newDeletedRanges || []
|
||||
}
|
||||
|
||||
private async runUserPromptSubmitHook(
|
||||
@@ -1134,7 +1145,10 @@ export class Task {
|
||||
previousState: {
|
||||
lastMessageTs: lastClineMessage?.ts?.toString() || "",
|
||||
messageCount: clineMessages.length.toString(),
|
||||
conversationHistoryDeleted: (this.taskState.conversationHistoryDeletedRange !== undefined).toString(),
|
||||
conversationHistoryDeleted: (
|
||||
this.taskState.conversationHistoryDeletedRanges !== undefined &&
|
||||
this.taskState.conversationHistoryDeletedRanges.length > 0
|
||||
).toString(),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1631,8 +1645,8 @@ export class Task {
|
||||
const hooksEnabled = this.stateManager.getGlobalSettingsKey("hooksEnabled")
|
||||
if (hooksEnabled) {
|
||||
try {
|
||||
// Calculate what the new deleted range will be
|
||||
const deletedRange = this.calculatePreCompactDeletedRange(apiConversationHistory)
|
||||
// Calculate what the new deleted ranges will be
|
||||
const deletedRanges = this.calculatePreCompactDeletedRanges(apiConversationHistory)
|
||||
|
||||
// Execute hook - throws HookCancellationError if cancelled
|
||||
await executePreCompactHookWithCleanup({
|
||||
@@ -1640,11 +1654,12 @@ export class Task {
|
||||
ulid: this.ulid,
|
||||
apiConversationHistory,
|
||||
conversationHistoryDeletedRange: this.taskState.conversationHistoryDeletedRange,
|
||||
conversationHistoryDeletedRanges: this.taskState.conversationHistoryDeletedRanges,
|
||||
contextManager: this.contextManager,
|
||||
clineMessages: this.messageStateHandler.getClineMessages(),
|
||||
messageStateHandler: this.messageStateHandler,
|
||||
compactionStrategy: "standard-truncation-lastquarter",
|
||||
deletedRange,
|
||||
deletedRanges,
|
||||
say: this.say.bind(this),
|
||||
setActiveHookExecution: async (hookExecution: HookExecution | undefined) => {
|
||||
if (hookExecution) {
|
||||
@@ -1669,13 +1684,13 @@ export class Task {
|
||||
}
|
||||
|
||||
// Proceed with standard truncation
|
||||
const newDeletedRange = this.contextManager.getNextTruncationRange(
|
||||
const newDeletedRanges = this.contextManager.getNextTruncationRange(
|
||||
apiConversationHistory,
|
||||
this.taskState.conversationHistoryDeletedRange,
|
||||
this.taskState.conversationHistoryDeletedRanges,
|
||||
"quarter", // Force aggressive truncation
|
||||
)
|
||||
|
||||
this.taskState.conversationHistoryDeletedRange = newDeletedRange
|
||||
this.taskState.conversationHistoryDeletedRanges = newDeletedRanges
|
||||
|
||||
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
|
||||
await this.contextManager.triggerApplyStandardContextTruncationNoticeChange(
|
||||
@@ -1794,16 +1809,16 @@ export class Task {
|
||||
this.messageStateHandler.getApiConversationHistory(),
|
||||
this.messageStateHandler.getClineMessages(),
|
||||
this.api,
|
||||
this.taskState.conversationHistoryDeletedRange,
|
||||
this.taskState.conversationHistoryDeletedRanges,
|
||||
previousApiReqIndex,
|
||||
await ensureTaskDirectoryExists(this.taskId),
|
||||
this.stateManager.getGlobalSettingsKey("useAutoCondense") && isNextGenModelFamily(this.api.getModel().id),
|
||||
)
|
||||
|
||||
if (contextManagementMetadata.updatedConversationHistoryDeletedRange) {
|
||||
this.taskState.conversationHistoryDeletedRange = contextManagementMetadata.conversationHistoryDeletedRange
|
||||
if (contextManagementMetadata.updatedConversationHistoryDeletedRanges) {
|
||||
this.taskState.conversationHistoryDeletedRanges = contextManagementMetadata.conversationHistoryDeletedRanges
|
||||
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
|
||||
// saves task history item which we use to keep track of conversation history deleted range
|
||||
// saves task history item which we use to keep track of conversation history deleted ranges
|
||||
}
|
||||
|
||||
// Response API requires native tool calls to be enabled
|
||||
@@ -1834,7 +1849,7 @@ export class Task {
|
||||
if (isContextWindowExceededError) {
|
||||
const truncatedConversationHistory = this.contextManager.getTruncatedMessages(
|
||||
this.messageStateHandler.getApiConversationHistory(),
|
||||
this.taskState.conversationHistoryDeletedRange,
|
||||
this.taskState.conversationHistoryDeletedRanges,
|
||||
)
|
||||
|
||||
// If the conversation has more than 3 messages, we can truncate again. If not, then the conversation is bricked.
|
||||
@@ -2274,14 +2289,17 @@ export class Task {
|
||||
if (this.taskState.currentlySummarizing) {
|
||||
this.taskState.currentlySummarizing = false
|
||||
|
||||
if (this.taskState.conversationHistoryDeletedRange) {
|
||||
const [start, end] = this.taskState.conversationHistoryDeletedRange
|
||||
if (
|
||||
this.taskState.conversationHistoryDeletedRanges &&
|
||||
this.taskState.conversationHistoryDeletedRanges.length > 0
|
||||
) {
|
||||
const [start, end] = this.taskState.conversationHistoryDeletedRanges[0]
|
||||
const apiHistory = this.messageStateHandler.getApiConversationHistory()
|
||||
|
||||
// we want to increment the deleted range to remove the pre-summarization tool call output, with additional safety check
|
||||
const safeEnd = Math.min(end + 2, apiHistory.length - 1)
|
||||
if (end + 2 <= safeEnd) {
|
||||
this.taskState.conversationHistoryDeletedRange = [start, end + 2]
|
||||
this.taskState.conversationHistoryDeletedRanges = [[start, end + 2]]
|
||||
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
|
||||
}
|
||||
}
|
||||
@@ -2299,9 +2317,13 @@ export class Task {
|
||||
// Edge case: summarize_task tool call completes but user cancels next request before it finishes.
|
||||
// This results in currentlySummarizing being false, and we fail to update the context window token estimate.
|
||||
// Check active message count to avoid summarizing a summary (bad UX but doesn't break logic).
|
||||
if (shouldCompact && this.taskState.conversationHistoryDeletedRange) {
|
||||
if (
|
||||
shouldCompact &&
|
||||
this.taskState.conversationHistoryDeletedRanges &&
|
||||
this.taskState.conversationHistoryDeletedRanges.length > 0
|
||||
) {
|
||||
const apiHistory = this.messageStateHandler.getApiConversationHistory()
|
||||
const activeMessageCount = apiHistory.length - this.taskState.conversationHistoryDeletedRange[1] - 1
|
||||
const activeMessageCount = apiHistory.length - this.taskState.conversationHistoryDeletedRanges[0][1] - 1
|
||||
|
||||
// IMPORTANT: We haven't appended the next user message yet, so the last message is an assistant message.
|
||||
// That's why we compare to even numbers (0, 2) rather than odd (1, 3).
|
||||
@@ -2314,7 +2336,7 @@ export class Task {
|
||||
if (shouldCompact) {
|
||||
shouldCompact = await this.contextManager.attemptFileReadOptimization(
|
||||
this.messageStateHandler.getApiConversationHistory(),
|
||||
this.taskState.conversationHistoryDeletedRange,
|
||||
this.taskState.conversationHistoryDeletedRanges,
|
||||
this.messageStateHandler.getClineMessages(),
|
||||
previousApiReqIndex,
|
||||
await ensureTaskDirectoryExists(this.taskId),
|
||||
|
||||
@@ -118,6 +118,7 @@ export class MessageStateHandler {
|
||||
shadowGitConfigWorkTree: await this.checkpointTracker?.getShadowGitConfigWorkTree(),
|
||||
cwdOnTaskInitialization: cwd,
|
||||
conversationHistoryDeletedRange: this.taskState.conversationHistoryDeletedRange,
|
||||
conversationHistoryDeletedRanges: this.taskState.conversationHistoryDeletedRanges,
|
||||
isFavorited: this.taskIsFavorited,
|
||||
checkpointManagerErrorMessage: this.taskState.checkpointManagerErrorMessage,
|
||||
modelId: lastModelInfo?.modelInfo?.modelId,
|
||||
@@ -165,6 +166,7 @@ export class MessageStateHandler {
|
||||
// it's important that apiConversationHistory is initialized before we add cline messages
|
||||
message.conversationHistoryIndex = this.apiConversationHistory.length - 1 // NOTE: this is the index of the last added message which is the user message, and once the clinemessages have been presented we update the apiconversationhistory with the completed assistant message. This means when resetting to a message, we need to +1 this index to get the correct assistant message that this tool use corresponds to
|
||||
message.conversationHistoryDeletedRange = this.taskState.conversationHistoryDeletedRange
|
||||
message.conversationHistoryDeletedRanges = this.taskState.conversationHistoryDeletedRanges
|
||||
this.clineMessages.push(message)
|
||||
await this.saveClineMessagesAndUpdateHistoryInternal()
|
||||
})
|
||||
|
||||
@@ -62,9 +62,9 @@ export class CondenseHandler implements IToolHandler, IPartialBlockHandler {
|
||||
const keepStrategy = summaryAlreadyAppended ? "lastTwo" : "none"
|
||||
|
||||
// clear the context history at this point in time
|
||||
config.taskState.conversationHistoryDeletedRange = config.services.contextManager.getNextTruncationRange(
|
||||
config.taskState.conversationHistoryDeletedRanges = config.services.contextManager.getNextTruncationRange(
|
||||
apiConversationHistory,
|
||||
config.taskState.conversationHistoryDeletedRange,
|
||||
config.taskState.conversationHistoryDeletedRanges,
|
||||
keepStrategy,
|
||||
)
|
||||
await config.messageState.saveClineMessagesAndUpdateHistory()
|
||||
|
||||
@@ -54,6 +54,7 @@ export class SummarizeTaskHandler implements IToolHandler, IPartialBlockHandler
|
||||
ulid: config.ulid,
|
||||
apiConversationHistory: apiHistory,
|
||||
conversationHistoryDeletedRange: config.taskState.conversationHistoryDeletedRange,
|
||||
conversationHistoryDeletedRanges: config.taskState.conversationHistoryDeletedRanges,
|
||||
contextManager: config.services.contextManager,
|
||||
clineMessages: config.messageState.getClineMessages(),
|
||||
messageStateHandler: config.messageState,
|
||||
@@ -221,9 +222,9 @@ export class SummarizeTaskHandler implements IToolHandler, IPartialBlockHandler
|
||||
|
||||
// clear the context history at this point in time. note that this will not include the assistant message
|
||||
// for summarizing, which we will need to delete later
|
||||
config.taskState.conversationHistoryDeletedRange = config.services.contextManager.getNextTruncationRange(
|
||||
config.taskState.conversationHistoryDeletedRanges = config.services.contextManager.getNextTruncationRange(
|
||||
apiConversationHistory,
|
||||
config.taskState.conversationHistoryDeletedRange,
|
||||
config.taskState.conversationHistoryDeletedRanges,
|
||||
keepStrategy,
|
||||
)
|
||||
await config.messageState.saveClineMessagesAndUpdateHistory()
|
||||
|
||||
@@ -46,6 +46,7 @@ type BuildArgs = {
|
||||
|
||||
// initial state for single-root
|
||||
initialConversationHistoryDeletedRange?: [number, number]
|
||||
initialConversationHistoryDeletedRanges?: Array<[number, number]>
|
||||
initialCheckpointManagerErrorMessage?: string
|
||||
|
||||
stateManager: StateManager
|
||||
@@ -69,6 +70,7 @@ export function buildCheckpointManager(args: BuildArgs): ICheckpointManager {
|
||||
cancelTask,
|
||||
postStateToWebview,
|
||||
initialConversationHistoryDeletedRange,
|
||||
initialConversationHistoryDeletedRanges,
|
||||
initialCheckpointManagerErrorMessage,
|
||||
stateManager,
|
||||
} = args
|
||||
@@ -99,6 +101,7 @@ export function buildCheckpointManager(args: BuildArgs): ICheckpointManager {
|
||||
},
|
||||
{
|
||||
conversationHistoryDeletedRange: initialConversationHistoryDeletedRange,
|
||||
conversationHistoryDeletedRanges: initialConversationHistoryDeletedRanges,
|
||||
checkpointManagerErrorMessage: initialCheckpointManagerErrorMessage,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -50,6 +50,7 @@ interface CheckpointManagerCallbacks {
|
||||
}
|
||||
interface CheckpointManagerInternalState {
|
||||
conversationHistoryDeletedRange?: [number, number]
|
||||
conversationHistoryDeletedRanges?: Array<[number, number]>
|
||||
checkpointTracker?: CheckpointTracker
|
||||
checkpointManagerErrorMessage?: string
|
||||
checkpointTrackerInitPromise?: Promise<CheckpointTracker | undefined>
|
||||
@@ -57,6 +58,7 @@ interface CheckpointManagerInternalState {
|
||||
|
||||
interface CheckpointRestoreStateUpdate {
|
||||
conversationHistoryDeletedRange?: [number, number]
|
||||
conversationHistoryDeletedRanges?: Array<[number, number]>
|
||||
checkpointManagerErrorMessage?: string
|
||||
}
|
||||
|
||||
@@ -363,6 +365,9 @@ export class TaskCheckpointManager implements ICheckpointManager {
|
||||
if (this.state.conversationHistoryDeletedRange !== undefined) {
|
||||
checkpointManagerStateUpdate.conversationHistoryDeletedRange = this.state.conversationHistoryDeletedRange
|
||||
}
|
||||
if (this.state.conversationHistoryDeletedRanges !== undefined) {
|
||||
checkpointManagerStateUpdate.conversationHistoryDeletedRanges = this.state.conversationHistoryDeletedRanges
|
||||
}
|
||||
} else {
|
||||
sendRelinquishControlEvent()
|
||||
|
||||
@@ -663,6 +668,8 @@ export class TaskCheckpointManager implements ICheckpointManager {
|
||||
// Update conversation history deleted range in our state
|
||||
this.state.conversationHistoryDeletedRange = message.conversationHistoryDeletedRange
|
||||
this.taskState.conversationHistoryDeletedRange = message.conversationHistoryDeletedRange
|
||||
this.state.conversationHistoryDeletedRanges = message.conversationHistoryDeletedRanges
|
||||
this.taskState.conversationHistoryDeletedRanges = message.conversationHistoryDeletedRanges
|
||||
|
||||
const apiConversationHistory = this.services.messageStateHandler.getApiConversationHistory()
|
||||
const newConversationHistory = apiConversationHistory.slice(0, (message.conversationHistoryIndex || 0) + 2) // +1 since this index corresponds to the last user message, and another +1 since slice end index is exclusive
|
||||
@@ -855,14 +862,6 @@ export class TaskCheckpointManager implements ICheckpointManager {
|
||||
// TODO - Future telemetry event capture here
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the conversation history deleted range
|
||||
*/
|
||||
updateConversationHistoryDeletedRange(range: [number, number] | undefined): void {
|
||||
this.state.conversationHistoryDeletedRange = range
|
||||
// TODO - Future telemetry event capture here
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Internal utilities - Private helpers for checkpoint operations
|
||||
// ============================================================================
|
||||
|
||||
@@ -125,6 +125,7 @@ export interface ClineMessage {
|
||||
isOperationOutsideWorkspace?: boolean
|
||||
conversationHistoryIndex?: number
|
||||
conversationHistoryDeletedRange?: [number, number] // for when conversation history is truncated for API requests
|
||||
conversationHistoryDeletedRanges?: Array<[number, number]> // for multiple non-contiguous deleted ranges
|
||||
modelInfo?: ClineMessageModelInfo
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ export type HistoryItem = {
|
||||
shadowGitConfigWorkTree?: string
|
||||
cwdOnTaskInitialization?: string
|
||||
conversationHistoryDeletedRange?: [number, number]
|
||||
conversationHistoryDeletedRanges?: Array<[number, number]>
|
||||
isFavorited?: boolean
|
||||
checkpointManagerErrorMessage?: string
|
||||
|
||||
|
||||
@@ -195,6 +195,11 @@ export function convertClineMessageToProto(message: AppClineMessage): ProtoCline
|
||||
endIndex: message.conversationHistoryDeletedRange[1],
|
||||
}
|
||||
: undefined,
|
||||
conversationHistoryDeletedRanges:
|
||||
message.conversationHistoryDeletedRanges?.map((range) => ({
|
||||
startIndex: range[0],
|
||||
endIndex: range[1],
|
||||
})) ?? [],
|
||||
// Additional optional fields for specific ask/say types
|
||||
sayTool: undefined,
|
||||
sayBrowserAction: undefined,
|
||||
@@ -272,5 +277,13 @@ export function convertProtoToClineMessage(protoMessage: ProtoClineMessage): App
|
||||
]
|
||||
}
|
||||
|
||||
// Convert conversationHistoryDeletedRanges from array of objects to array of tuples
|
||||
if (protoMessage.conversationHistoryDeletedRanges.length > 0) {
|
||||
message.conversationHistoryDeletedRanges = protoMessage.conversationHistoryDeletedRanges.map((range) => [
|
||||
range.startIndex,
|
||||
range.endIndex,
|
||||
])
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user