Skip to content
17 changes: 17 additions & 0 deletions packages/types/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export const clineSays = [
"diff_error",
"condense_context",
"condense_context_error",
"sliding_window_truncation",
"codebase_search_result",
"user_edit_todos",
] as const
Expand Down Expand Up @@ -203,10 +204,25 @@ export const contextCondenseSchema = z.object({
prevContextTokens: z.number(),
newContextTokens: z.number(),
summary: z.string(),
condenseId: z.string().optional(),
})

export type ContextCondense = z.infer<typeof contextCondenseSchema>

/**
* ContextTruncation
*
* Used to track sliding window truncation events for the UI.
*/

export const contextTruncationSchema = z.object({
truncationId: z.string(),
messagesRemoved: z.number(),
prevContextTokens: z.number(),
})

export type ContextTruncation = z.infer<typeof contextTruncationSchema>

/**
* ClineMessage
*/
Expand All @@ -224,6 +240,7 @@ export const clineMessageSchema = z.object({
checkpoint: z.record(z.string(), z.unknown()).optional(),
progressStatus: toolProgressStatusSchema.optional(),
contextCondense: contextCondenseSchema.optional(),
contextTruncation: contextTruncationSchema.optional(),
isProtected: z.boolean().optional(),
apiProtocol: z.union([z.literal("openai"), z.literal("anthropic")]).optional(),
isAnswered: z.boolean().optional(),
Expand Down
17 changes: 12 additions & 5 deletions src/core/condense/__tests__/condense.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import { TelemetryService } from "@roo-code/telemetry"

import { BaseProvider } from "../../../api/providers/base-provider"
import { ApiMessage } from "../../task-persistence/apiMessages"
import { summarizeConversation, getMessagesSinceLastSummary, N_MESSAGES_TO_KEEP } from "../index"
import {
summarizeConversation,
getMessagesSinceLastSummary,
getEffectiveApiHistory,
N_MESSAGES_TO_KEEP,
} from "../index"

// Create a mock ApiHandler for testing
class MockApiHandler extends BaseProvider {
Expand Down Expand Up @@ -83,11 +88,13 @@ describe("Condense", () => {
expect(summaryMessage).toBeTruthy()
expect(summaryMessage?.content).toBe("Mock summary of the conversation")

// Verify we have the expected number of messages
// [first message, summary, last N messages]
expect(result.messages.length).toBe(1 + 1 + N_MESSAGES_TO_KEEP)
// With non-destructive condensing, all messages are retained (tagged but not deleted)
// Use getEffectiveApiHistory to verify the effective view matches the old behavior
expect(result.messages.length).toBe(messages.length + 1) // All original messages + summary
const effectiveHistory = getEffectiveApiHistory(result.messages)
expect(effectiveHistory.length).toBe(1 + 1 + N_MESSAGES_TO_KEEP) // first + summary + last N

// Verify the last N messages are preserved
// Verify the last N messages are preserved (same messages by reference)
const lastMessages = result.messages.slice(-N_MESSAGES_TO_KEEP)
expect(lastMessages).toEqual(messages.slice(-N_MESSAGES_TO_KEEP))
})
Expand Down
66 changes: 41 additions & 25 deletions src/core/condense/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
summarizeConversation,
getMessagesSinceLastSummary,
getKeepMessagesWithToolBlocks,
getEffectiveApiHistory,
cleanupAfterTruncation,
N_MESSAGES_TO_KEEP,
} from "../index"

Expand Down Expand Up @@ -407,22 +409,28 @@ describe("summarizeConversation", () => {
expect(mockApiHandler.createMessage).toHaveBeenCalled()
expect(maybeRemoveImageBlocks).toHaveBeenCalled()

// Verify the structure of the result
// The result should be: first message + summary + last N messages
expect(result.messages.length).toBe(1 + 1 + N_MESSAGES_TO_KEEP) // First + summary + last N
// With non-destructive condensing, the result contains ALL original messages
// plus the summary message. Condensed messages are tagged but not deleted.
// Use getEffectiveApiHistory to verify the effective API view matches the old behavior.
expect(result.messages.length).toBe(messages.length + 1) // All original messages + summary

// Check that the first message is preserved
expect(result.messages[0]).toEqual(messages[0])

// Check that the summary message was inserted correctly
const summaryMessage = result.messages[1]
expect(summaryMessage.role).toBe("assistant")
expect(summaryMessage.content).toBe("This is a summary")
expect(summaryMessage.isSummary).toBe(true)
// Find the summary message (it has isSummary: true)
const summaryMessage = result.messages.find((m) => m.isSummary)
expect(summaryMessage).toBeDefined()
expect(summaryMessage!.role).toBe("assistant")
expect(summaryMessage!.content).toBe("This is a summary")
expect(summaryMessage!.isSummary).toBe(true)

// Check that the last N_MESSAGES_TO_KEEP messages are preserved
const lastMessages = messages.slice(-N_MESSAGES_TO_KEEP)
expect(result.messages.slice(-N_MESSAGES_TO_KEEP)).toEqual(lastMessages)
// Verify that the effective API history matches expected: first + summary + last N messages
const effectiveHistory = getEffectiveApiHistory(result.messages)
expect(effectiveHistory.length).toBe(1 + 1 + N_MESSAGES_TO_KEEP) // First + summary + last N

// Check that condensed messages are properly tagged
const condensedMessages = result.messages.filter((m) => m.condenseParent !== undefined)
expect(condensedMessages.length).toBeGreaterThan(0)

// Check the cost and token counts
expect(result.cost).toBe(0.05)
Expand Down Expand Up @@ -643,9 +651,11 @@ describe("summarizeConversation", () => {
prevContextTokens,
)

// Should successfully summarize
// Result should be: first message + summary + last N messages
expect(result.messages.length).toBe(1 + 1 + N_MESSAGES_TO_KEEP) // First + summary + last N
// With non-destructive condensing, result contains all messages plus summary
// Use getEffectiveApiHistory to verify the effective API view
expect(result.messages.length).toBe(messages.length + 1) // All messages + summary
const effectiveHistory = getEffectiveApiHistory(result.messages)
expect(effectiveHistory.length).toBe(1 + 1 + N_MESSAGES_TO_KEEP) // First + summary + last N
expect(result.cost).toBe(0.03)
expect(result.summary).toBe("Concise summary")
expect(result.error).toBeUndefined()
Expand Down Expand Up @@ -809,23 +819,27 @@ describe("summarizeConversation", () => {
true, // useNativeTools - required for tool_use block preservation
)

// Verify the summary message has content array with text and tool_use blocks
const summaryMessage = result.messages[1]
expect(summaryMessage.role).toBe("assistant")
expect(summaryMessage.isSummary).toBe(true)
expect(Array.isArray(summaryMessage.content)).toBe(true)
// Find the summary message
const summaryMessage = result.messages.find((m) => m.isSummary)
expect(summaryMessage).toBeDefined()
expect(summaryMessage!.role).toBe("assistant")
expect(summaryMessage!.isSummary).toBe(true)
expect(Array.isArray(summaryMessage!.content)).toBe(true)

// Content should be [text block, tool_use block]
const content = summaryMessage.content as any[]
const content = summaryMessage!.content as any[]
expect(content).toHaveLength(2)
expect(content[0].type).toBe("text")
expect(content[0].text).toBe("Summary of conversation")
expect(content[1].type).toBe("tool_use")
expect(content[1].id).toBe("toolu_123")
expect(content[1].name).toBe("read_file")

// Verify the keepMessages are preserved correctly
expect(result.messages).toHaveLength(1 + 1 + N_MESSAGES_TO_KEEP) // first + summary + last 3
// With non-destructive condensing, all messages are retained plus the summary
expect(result.messages.length).toBe(messages.length + 1) // all original + summary
// Verify effective history matches expected
const effectiveHistory = getEffectiveApiHistory(result.messages)
expect(effectiveHistory.length).toBe(1 + 1 + N_MESSAGES_TO_KEEP) // first + summary + last 3
expect(result.error).toBeUndefined()
})

Expand Down Expand Up @@ -961,9 +975,11 @@ describe("summarizeConversation", () => {
true,
)

const summaryMessage = result.messages[1]
expect(Array.isArray(summaryMessage.content)).toBe(true)
const summaryContent = summaryMessage.content as any[]
// Find the summary message (it has isSummary: true)
const summaryMessage = result.messages.find((m) => m.isSummary)
expect(summaryMessage).toBeDefined()
expect(Array.isArray(summaryMessage!.content)).toBe(true)
const summaryContent = summaryMessage!.content as any[]
expect(summaryContent[0]).toEqual({ type: "text", text: "This is a summary" })

const preservedToolUses = summaryContent.filter((block) => block.type === "tool_use")
Expand Down
Loading
Loading