Compare commits

...
16 changed files with 159 additions and 88 deletions
+13 -2
View File
@@ -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;
}
+2
View File
@@ -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)
+14 -12
View File
@@ -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,
+1
View File
@@ -34,6 +34,7 @@ export class TaskState {
// Context and history
conversationHistoryDeletedRange?: [number, number]
conversationHistoryDeletedRanges?: Array<[number, number]>
// Tool execution flags
didRejectTool = false
+46 -24
View File
@@ -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),
+2
View File
@@ -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()
+3
View File
@@ -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,
},
)
+7 -8
View File
@@ -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
// ============================================================================
+1
View File
@@ -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
}
+1
View File
@@ -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
}