diff --git a/src/core/condense/__tests__/condense.spec.ts b/src/core/condense/__tests__/condense.spec.ts index dcb05cd74df..9d3352d01a5 100644 --- a/src/core/condense/__tests__/condense.spec.ts +++ b/src/core/condense/__tests__/condense.spec.ts @@ -250,7 +250,7 @@ Line 2 it("should not summarize messages that already contain a recent summary with no new messages", async () => { const messages: ApiMessage[] = [ { role: "user", content: "First message with /command" }, - { role: "assistant", content: "Previous summary", isSummary: true }, + { role: "user", content: "Previous summary", isSummary: true }, ] const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, false) @@ -413,20 +413,5 @@ Line 2 expect(result[1]).toEqual(messages[4]) expect(result[2]).toEqual(messages[5]) }) - - it("should prepend first user message when summary starts with assistant", () => { - const messages: ApiMessage[] = [ - { role: "user", content: "Original first message" }, - { role: "assistant", content: "Summary content", isSummary: true }, - { role: "user", content: "After summary" }, - ] - - const result = getMessagesSinceLastSummary(messages) - - // Should prepend original first message for Bedrock compatibility - expect(result[0]).toEqual(messages[0]) // Original first user message - expect(result[1]).toEqual(messages[1]) // The summary - expect(result[2]).toEqual(messages[2]) - }) }) }) diff --git a/src/core/condense/__tests__/index.spec.ts b/src/core/condense/__tests__/index.spec.ts index 8a0f3bea63a..c8bc5ee8ef1 100644 --- a/src/core/condense/__tests__/index.spec.ts +++ b/src/core/condense/__tests__/index.spec.ts @@ -252,38 +252,36 @@ describe("getMessagesSinceLastSummary", () => { expect(result).toEqual(messages) }) - it("should return messages since the last summary (preserves original first user message when needed)", () => { + it("should return messages since the last summary", () => { const messages: ApiMessage[] = [ { role: "user", content: "Hello", ts: 1 }, { role: "assistant", content: "Hi there", ts: 2 }, - { role: "assistant", content: "Summary of conversation", ts: 3, isSummary: true }, - { role: "user", content: "How are you?", ts: 4 }, - { role: "assistant", content: "I'm good", ts: 5 }, + { role: "user", content: "Summary of conversation", ts: 3, isSummary: true }, + { role: "assistant", content: "How are you?", ts: 4 }, + { role: "user", content: "I'm good", ts: 5 }, ] const result = getMessagesSinceLastSummary(messages) expect(result).toEqual([ - { role: "user", content: "Hello", ts: 1 }, - { role: "assistant", content: "Summary of conversation", ts: 3, isSummary: true }, - { role: "user", content: "How are you?", ts: 4 }, - { role: "assistant", content: "I'm good", ts: 5 }, + { role: "user", content: "Summary of conversation", ts: 3, isSummary: true }, + { role: "assistant", content: "How are you?", ts: 4 }, + { role: "user", content: "I'm good", ts: 5 }, ]) }) it("should handle multiple summary messages and return since the last one", () => { const messages: ApiMessage[] = [ { role: "user", content: "Hello", ts: 1 }, - { role: "assistant", content: "First summary", ts: 2, isSummary: true }, - { role: "user", content: "How are you?", ts: 3 }, - { role: "assistant", content: "Second summary", ts: 4, isSummary: true }, - { role: "user", content: "What's new?", ts: 5 }, + { role: "user", content: "First summary", ts: 2, isSummary: true }, + { role: "assistant", content: "How are you?", ts: 3 }, + { role: "user", content: "Second summary", ts: 4, isSummary: true }, + { role: "assistant", content: "What's new?", ts: 5 }, ] const result = getMessagesSinceLastSummary(messages) expect(result).toEqual([ - { role: "user", content: "Hello", ts: 1 }, - { role: "assistant", content: "Second summary", ts: 4, isSummary: true }, - { role: "user", content: "What's new?", ts: 5 }, + { role: "user", content: "Second summary", ts: 4, isSummary: true }, + { role: "assistant", content: "What's new?", ts: 5 }, ]) }) diff --git a/src/core/condense/__tests__/nested-condense.spec.ts b/src/core/condense/__tests__/nested-condense.spec.ts new file mode 100644 index 00000000000..3868a22262b --- /dev/null +++ b/src/core/condense/__tests__/nested-condense.spec.ts @@ -0,0 +1,211 @@ +import { describe, it, expect } from "vitest" +import { ApiMessage } from "../../task-persistence/apiMessages" +import { getEffectiveApiHistory, getMessagesSinceLastSummary } from "../index" + +describe("nested condensing scenarios", () => { + describe("fresh-start model (user-role summaries)", () => { + it("should return only the latest summary and messages after it", () => { + const condenseId1 = "condense-1" + const condenseId2 = "condense-2" + + // Simulate history after two nested condenses with user-role summaries + const history: ApiMessage[] = [ + // Original task - condensed in first condense + { role: "user", content: "Build an app", ts: 100, condenseParent: condenseId1 }, + // Messages from first condense + { role: "assistant", content: "Starting...", ts: 200, condenseParent: condenseId1 }, + { role: "user", content: "Add auth", ts: 300, condenseParent: condenseId1 }, + // First summary (user role, fresh-start model) - then condensed in second condense + { + role: "user", + content: [{ type: "text", text: "## Summary 1" }], + ts: 399, + isSummary: true, + condenseId: condenseId1, + condenseParent: condenseId2, // Tagged during second condense + }, + // Messages after first condense but before second + { role: "assistant", content: "Auth added", ts: 400, condenseParent: condenseId2 }, + { role: "user", content: "Add database", ts: 500, condenseParent: condenseId2 }, + // Second summary (user role, fresh-start model) + { + role: "user", + content: [{ type: "text", text: "## Summary 2" }], + ts: 599, + isSummary: true, + condenseId: condenseId2, + }, + // Messages after second condense (kept messages) + { role: "assistant", content: "Database added", ts: 600 }, + { role: "user", content: "Now test it", ts: 700 }, + ] + + // Step 1: Get effective history + const effectiveHistory = getEffectiveApiHistory(history) + + // Should only contain: Summary2, and messages after it + expect(effectiveHistory.length).toBe(3) + expect(effectiveHistory[0].isSummary).toBe(true) + expect(effectiveHistory[0].condenseId).toBe(condenseId2) // Latest summary + expect(effectiveHistory[1].content).toBe("Database added") + expect(effectiveHistory[2].content).toBe("Now test it") + + // Verify NO condensed messages are included + const hasCondensedMessages = effectiveHistory.some( + (msg) => msg.condenseParent && history.some((m) => m.isSummary && m.condenseId === msg.condenseParent), + ) + expect(hasCondensedMessages).toBe(false) + + // Step 2: Get messages since last summary (on effective history) + const messagesSinceLastSummary = getMessagesSinceLastSummary(effectiveHistory) + + // Should be the same as effective history since Summary2 is already at the start + expect(messagesSinceLastSummary.length).toBe(3) + expect(messagesSinceLastSummary[0].isSummary).toBe(true) + expect(messagesSinceLastSummary[0].condenseId).toBe(condenseId2) + + // CRITICAL: No previous history (Summary1 or original task) should be included + const hasSummary1 = messagesSinceLastSummary.some((m) => m.condenseId === condenseId1) + expect(hasSummary1).toBe(false) + + const hasOriginalTask = messagesSinceLastSummary.some((m) => m.content === "Build an app") + expect(hasOriginalTask).toBe(false) + }) + + it("should handle triple nested condense correctly", () => { + const condenseId1 = "condense-1" + const condenseId2 = "condense-2" + const condenseId3 = "condense-3" + + const history: ApiMessage[] = [ + // First condense content + { role: "user", content: "Task", ts: 100, condenseParent: condenseId1 }, + { + role: "user", + content: [{ type: "text", text: "## Summary 1" }], + ts: 199, + isSummary: true, + condenseId: condenseId1, + condenseParent: condenseId2, + }, + // Second condense content + { role: "assistant", content: "After S1", ts: 200, condenseParent: condenseId2 }, + { + role: "user", + content: [{ type: "text", text: "## Summary 2" }], + ts: 299, + isSummary: true, + condenseId: condenseId2, + condenseParent: condenseId3, + }, + // Third condense content + { role: "assistant", content: "After S2", ts: 300, condenseParent: condenseId3 }, + { + role: "user", + content: [{ type: "text", text: "## Summary 3" }], + ts: 399, + isSummary: true, + condenseId: condenseId3, + }, + // Current messages + { role: "assistant", content: "Current work", ts: 400 }, + ] + + const effectiveHistory = getEffectiveApiHistory(history) + + // Should only contain Summary3 and current work + expect(effectiveHistory.length).toBe(2) + expect(effectiveHistory[0].condenseId).toBe(condenseId3) + expect(effectiveHistory[1].content).toBe("Current work") + + const messagesSinceLastSummary = getMessagesSinceLastSummary(effectiveHistory) + expect(messagesSinceLastSummary.length).toBe(2) + + // No previous summaries should be included + const hasPreviousSummaries = messagesSinceLastSummary.some( + (m) => m.condenseId === condenseId1 || m.condenseId === condenseId2, + ) + expect(hasPreviousSummaries).toBe(false) + }) + }) + + describe("getMessagesSinceLastSummary behavior with full vs effective history", () => { + it("should return consistent results when called with full history vs effective history", () => { + const condenseId = "condense-1" + + const fullHistory: ApiMessage[] = [ + { role: "user", content: "Original task", ts: 100, condenseParent: condenseId }, + { role: "assistant", content: "Response", ts: 200, condenseParent: condenseId }, + { + role: "user", + content: [{ type: "text", text: "Summary" }], + ts: 299, + isSummary: true, + condenseId, + }, + { role: "assistant", content: "After summary", ts: 300 }, + ] + + // Called with FULL history (as in summarizeConversation) + const fromFullHistory = getMessagesSinceLastSummary(fullHistory) + + // Called with EFFECTIVE history (as in attemptApiRequest) + const effectiveHistory = getEffectiveApiHistory(fullHistory) + const fromEffectiveHistory = getMessagesSinceLastSummary(effectiveHistory) + + // Both should return the same messages when summary is user role + expect(fromFullHistory.length).toBe(fromEffectiveHistory.length) + + // Both should start with the summary + expect(fromFullHistory[0].isSummary).toBe(true) + expect(fromEffectiveHistory[0].isSummary).toBe(true) + }) + + it("should not include condensed original task in effective history", () => { + const condenseId1 = "condense-1" + const condenseId2 = "condense-2" + + // Scenario: Two nested condenses with user-role summaries + const fullHistory: ApiMessage[] = [ + { role: "user", content: "Original task - should NOT appear", ts: 100, condenseParent: condenseId1 }, + { role: "assistant", content: "Old response", ts: 200, condenseParent: condenseId1 }, + // First summary (user role, fresh-start model), then condensed again + { + role: "user", + content: [{ type: "text", text: "Summary 1" }], + ts: 299, + isSummary: true, + condenseId: condenseId1, + condenseParent: condenseId2, + }, + { role: "assistant", content: "After S1", ts: 300, condenseParent: condenseId2 }, + // Second summary (user role, fresh-start model) + { + role: "user", + content: [{ type: "text", text: "Summary 2" }], + ts: 399, + isSummary: true, + condenseId: condenseId2, + }, + { role: "assistant", content: "Current message", ts: 400 }, + ] + + const effectiveHistory = getEffectiveApiHistory(fullHistory) + expect(effectiveHistory.length).toBe(2) // Summary2 + Current message + + const messagesSinceLastSummary = getMessagesSinceLastSummary(effectiveHistory) + + // The original task should NOT be included + const hasOriginalTask = messagesSinceLastSummary.some((m) => + typeof m.content === "string" + ? m.content.includes("Original task") + : JSON.stringify(m.content).includes("Original task"), + ) + expect(hasOriginalTask).toBe(false) + + // Summary1 should not be included (it was condensed) + const hasSummary1 = messagesSinceLastSummary.some((m) => m.condenseId === condenseId1) + expect(hasSummary1).toBe(false) + }) + }) +}) diff --git a/src/core/condense/__tests__/rewind-after-condense.spec.ts b/src/core/condense/__tests__/rewind-after-condense.spec.ts index 84fdb63ca86..068f49a8570 100644 --- a/src/core/condense/__tests__/rewind-after-condense.spec.ts +++ b/src/core/condense/__tests__/rewind-after-condense.spec.ts @@ -83,7 +83,7 @@ describe("Rewind After Condense - Issue #8295", () => { const messages: ApiMessage[] = [ { role: "user", content: "First message", ts: 1 }, { role: "assistant", content: "First response", ts: 2, condenseParent: condenseId }, - { role: "assistant", content: "Summary", ts: 3, isSummary: true, condenseId }, + { role: "user", content: "Summary", ts: 3, isSummary: true, condenseId }, ] const cleaned = cleanupAfterTruncation(messages) @@ -97,7 +97,7 @@ describe("Rewind After Condense - Issue #8295", () => { const condenseId2 = "summary-2" const messages: ApiMessage[] = [ { role: "user", content: "Message 1", ts: 1, condenseParent: condenseId1 }, - { role: "assistant", content: "Summary 1", ts: 2, isSummary: true, condenseId: condenseId1 }, + { role: "user", content: "Summary 1", ts: 2, isSummary: true, condenseId: condenseId1 }, { role: "user", content: "Message 2", ts: 3, condenseParent: condenseId2 }, // Summary 2 is NOT present (was truncated) ] @@ -203,8 +203,8 @@ describe("Rewind After Condense - Issue #8295", () => { { role: "user", content: "Start", ts: 1 }, { role: "assistant", content: "Response 1", ts: 2, condenseParent: condenseId }, { role: "user", content: "More", ts: 3, condenseParent: condenseId }, - { role: "assistant", content: "Summary", ts: 4, isSummary: true, condenseId }, - { role: "user", content: "After summary", ts: 5 }, + { role: "user", content: "Summary", ts: 4, isSummary: true, condenseId }, + { role: "assistant", content: "After summary", ts: 5 }, ] // Fresh start model: effective history is summary + messages after it @@ -250,7 +250,7 @@ describe("Rewind After Condense - Issue #8295", () => { { role: "assistant", content: "Response 3", ts: 600, condenseParent: condenseId }, { role: "user", content: "Even more", ts: 700, condenseParent: condenseId }, // Summary gets ts = firstKeptTs - 1 = 999, which is unique - { role: "assistant", content: "Summary", ts: firstKeptTs - 1, isSummary: true, condenseId }, + { role: "user", content: "Summary", ts: firstKeptTs - 1, isSummary: true, condenseId }, // First kept message { role: "user", content: "First kept message", ts: firstKeptTs }, { role: "assistant", content: "Response to first kept", ts: 1100 }, @@ -283,9 +283,9 @@ describe("Rewind After Condense - Issue #8295", () => { const messages: ApiMessage[] = [ { role: "user", content: "Initial", ts: 1 }, - { role: "assistant", content: "Summary", ts: firstKeptTs - 1, isSummary: true, condenseId }, - { role: "user", content: "First kept message", ts: firstKeptTs }, - { role: "assistant", content: "Response", ts: 9 }, + { role: "user", content: "Summary", ts: firstKeptTs - 1, isSummary: true, condenseId }, + { role: "assistant", content: "First kept message", ts: firstKeptTs }, + { role: "user", content: "Response", ts: 9 }, ] // Look up by first kept message's timestamp @@ -330,7 +330,7 @@ describe("Rewind After Condense - Issue #8295", () => { { role: "user", content: "Now the tests", ts: 700, condenseParent: condenseId }, // Summary inserted before first kept message { - role: "assistant", + role: "user", content: "Summary: Built API with validation, working on tests", ts: 799, // msg8.ts - 1 isSummary: true, @@ -345,12 +345,12 @@ describe("Rewind After Condense - Issue #8295", () => { const effective = getEffectiveApiHistory(storageAfterCondense) // Should send exactly 4 messages to LLM: - // 1. Summary (assistant) + // 1. Summary (user) // 2-4. Last 3 kept messages expect(effective.length).toBe(4) // Verify exact order and content - expect(effective[0].role).toBe("assistant") + expect(effective[0].role).toBe("user") expect(effective[0].isSummary).toBe(true) expect(effective[0].content).toBe("Summary: Built API with validation, working on tests") @@ -394,7 +394,7 @@ describe("Rewind After Condense - Issue #8295", () => { // First summary - now ALSO tagged with condenseId2 (from second condense) { - role: "assistant", + role: "user", content: "Summary1: Built auth and database", ts: 799, isSummary: true, @@ -416,7 +416,7 @@ describe("Rewind After Condense - Issue #8295", () => { // Second summary - inserted before the last 3 kept messages { - role: "assistant", + role: "user", content: "Summary2: App complete with auth, DB, API, validation, errors, logging. Now testing.", ts: 1799, // msg18.ts - 1 isSummary: true, @@ -432,12 +432,12 @@ describe("Rewind After Condense - Issue #8295", () => { const effective = getEffectiveApiHistory(storageAfterDoubleCondense) // Should send exactly 4 messages to LLM: - // 1. Summary2 (assistant) - the ACTIVE summary + // 1. Summary2 (user) - the ACTIVE summary // 2-4. Last 3 kept messages expect(effective.length).toBe(4) // Verify exact order and content - expect(effective[0].role).toBe("assistant") + expect(effective[0].role).toBe("user") expect(effective[0].isSummary).toBe(true) expect(effective[0].condenseId).toBe(condenseId2) // Must be the SECOND summary expect(effective[0].content).toContain("Summary2") @@ -477,7 +477,7 @@ describe("Rewind After Condense - Issue #8295", () => { { role: "user", content: "Start task", ts: 100, condenseParent: condenseId }, { role: "assistant", content: "Response 1", ts: 200, condenseParent: condenseId }, { role: "user", content: "Continue", ts: 300, condenseParent: condenseId }, - { role: "assistant", content: "Summary text", ts: 399, isSummary: true, condenseId }, + { role: "user", content: "Summary text", ts: 399, isSummary: true, condenseId }, // Kept messages - should alternate properly { role: "assistant", content: "Response after summary", ts: 400 }, { role: "user", content: "User message", ts: 500 }, @@ -486,10 +486,9 @@ describe("Rewind After Condense - Issue #8295", () => { const effective = getEffectiveApiHistory(storage) - // Verify the sequence: assistant(summary), assistant, user, assistant - // Note: Having two assistant messages in a row (summary + next response) is valid - // because the summary replaces what would have been multiple messages - expect(effective[0].role).toBe("assistant") + // Verify the sequence: user(summary), assistant, user, assistant + // This is the fresh-start model with user-role summaries + expect(effective[0].role).toBe("user") expect(effective[0].isSummary).toBe(true) expect(effective[1].role).toBe("assistant") expect(effective[2].role).toBe("user") @@ -502,10 +501,10 @@ describe("Rewind After Condense - Issue #8295", () => { const storage: ApiMessage[] = [ { role: "user", content: "First", ts: 100, condenseParent: condenseId }, { role: "assistant", content: "Condensed", ts: 200, condenseParent: condenseId }, - { role: "assistant", content: "Summary", ts: 299, isSummary: true, condenseId }, - { role: "user", content: "Kept 1", ts: 300 }, - { role: "assistant", content: "Kept 2", ts: 400 }, - { role: "user", content: "Kept 3", ts: 500 }, + { role: "user", content: "Summary", ts: 299, isSummary: true, condenseId }, + { role: "assistant", content: "Kept 1", ts: 300 }, + { role: "user", content: "Kept 2", ts: 400 }, + { role: "assistant", content: "Kept 3", ts: 500 }, ] const effective = getEffectiveApiHistory(storage) diff --git a/src/core/condense/index.ts b/src/core/condense/index.ts index 0c92087aff1..313bfcebb6b 100644 --- a/src/core/condense/index.ts +++ b/src/core/condense/index.ts @@ -372,38 +372,22 @@ ${commandBlocks} return { messages: newMessages, summary, cost, newContextTokens, condenseId } } -/* Returns the list of all messages since the last summary message, including the summary. Returns all messages if there is no summary. */ +/** + * Returns the list of all messages since the last summary message, including the summary. + * Returns all messages if there is no summary. + * + * Note: Summary messages are always created with role: "user" (fresh-start model), + * so the first message since the last summary is guaranteed to be a user message. + */ export function getMessagesSinceLastSummary(messages: ApiMessage[]): ApiMessage[] { - let lastSummaryIndexReverse = [...messages].reverse().findIndex((message) => message.isSummary) + const lastSummaryIndexReverse = [...messages].reverse().findIndex((message) => message.isSummary) if (lastSummaryIndexReverse === -1) { return messages } const lastSummaryIndex = messages.length - lastSummaryIndexReverse - 1 - const messagesSinceSummary = messages.slice(lastSummaryIndex) - - // Bedrock requires the first message to be a user message. - // We preserve the original first message to maintain context. - // See https://github.com/RooCodeInc/Roo-Code/issues/4147 - if (messagesSinceSummary.length > 0 && messagesSinceSummary[0].role !== "user") { - // Get the original first message (should always be a user message with the task) - const originalFirstMessage = messages[0] - if (originalFirstMessage && originalFirstMessage.role === "user") { - // Use the original first message unchanged to maintain full context - return [originalFirstMessage, ...messagesSinceSummary] - } else { - // Fallback to generic message if no original first message exists (shouldn't happen) - const userMessage: ApiMessage = { - role: "user", - content: "Please continue from the following summary:", - ts: messages[0]?.ts ? messages[0].ts - 1 : Date.now(), - } - return [userMessage, ...messagesSinceSummary] - } - } - - return messagesSinceSummary + return messages.slice(lastSummaryIndex) } /** diff --git a/src/core/context-management/__tests__/context-management.spec.ts b/src/core/context-management/__tests__/context-management.spec.ts index 7c5db2d5104..2616ea571df 100644 --- a/src/core/context-management/__tests__/context-management.spec.ts +++ b/src/core/context-management/__tests__/context-management.spec.ts @@ -578,8 +578,8 @@ describe("Context Management", () => { const mockSummarizeResponse: condenseModule.SummarizeResponse = { messages: [ { role: "user", content: "First message" }, - { role: "assistant", content: mockSummary, isSummary: true }, - { role: "user", content: "Last message" }, + { role: "user", content: mockSummary, isSummary: true }, + { role: "assistant", content: "Last message" }, ], summary: mockSummary, cost: mockCost, @@ -751,8 +751,8 @@ describe("Context Management", () => { const mockSummarizeResponse: condenseModule.SummarizeResponse = { messages: [ { role: "user", content: "First message" }, - { role: "assistant", content: mockSummary, isSummary: true }, - { role: "user", content: "Last message" }, + { role: "user", content: mockSummary, isSummary: true }, + { role: "assistant", content: "Last message" }, ], summary: mockSummary, cost: mockCost, @@ -899,8 +899,8 @@ describe("Context Management", () => { const mockSummarizeResponse: condenseModule.SummarizeResponse = { messages: [ { role: "user", content: "First message" }, - { role: "assistant", content: mockSummary, isSummary: true }, - { role: "user", content: "Last message" }, + { role: "user", content: mockSummary, isSummary: true }, + { role: "assistant", content: "Last message" }, ], summary: mockSummary, cost: mockCost, @@ -965,8 +965,8 @@ describe("Context Management", () => { const mockSummarizeResponse: condenseModule.SummarizeResponse = { messages: [ { role: "user", content: "First message" }, - { role: "assistant", content: mockSummary, isSummary: true }, - { role: "user", content: "Last message" }, + { role: "user", content: mockSummary, isSummary: true }, + { role: "assistant", content: "Last message" }, ], summary: mockSummary, cost: mockCost, diff --git a/src/core/webview/__tests__/webviewMessageHandler.delete.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.delete.spec.ts index 541a7106212..1af6b43bc15 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.delete.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.delete.spec.ts @@ -267,7 +267,7 @@ describe("webviewMessageHandler delete functionality", () => { { ts: 100, role: "user", content: "First message", condenseParent: condenseId }, { ts: 200, role: "assistant", content: "Response 1", condenseParent: condenseId }, { ts: 300, role: "user", content: "Second message", condenseParent: condenseId }, - { ts: 799, role: "assistant", content: "Summary", isSummary: true, condenseId }, + { ts: 799, role: "user", content: "Summary", isSummary: true, condenseId }, { ts: 800, role: "assistant", content: "Kept message 1" }, { ts: 900, role: "user", content: "Kept message 2" }, { ts: 1000, role: "assistant", content: "Kept message 3" }, @@ -314,8 +314,8 @@ describe("webviewMessageHandler delete functionality", () => { { ts: 100, role: "user", content: "Task start", condenseParent: condenseId }, { ts: 200, role: "assistant", content: "Response 1", condenseParent: condenseId }, { ts: 300, role: "user", content: "Message 2", condenseParent: condenseId }, - { ts: 999, role: "assistant", content: "Summary", isSummary: true, condenseId }, - { ts: 1000, role: "user", content: "First kept" }, + { ts: 999, role: "user", content: "Summary", isSummary: true, condenseId }, + { ts: 1000, role: "assistant", content: "First kept" }, ] // Delete "Message 2" (ts=300) - this removes summary too, so orphaned tags should be cleared @@ -357,7 +357,7 @@ describe("webviewMessageHandler delete functionality", () => { // First summary - ALSO tagged with condenseId2 from second condense { ts: 799, - role: "assistant", + role: "user", content: "Summary1", isSummary: true, condenseId: condenseId1, @@ -367,7 +367,7 @@ describe("webviewMessageHandler delete functionality", () => { { ts: 1000, role: "assistant", content: "Msg after summary1", condenseParent: condenseId2 }, { ts: 1100, role: "user", content: "More msgs", condenseParent: condenseId2 }, // Second summary - { ts: 1799, role: "assistant", content: "Summary2", isSummary: true, condenseId: condenseId2 }, + { ts: 1799, role: "user", content: "Summary2", isSummary: true, condenseId: condenseId2 }, // Kept messages { ts: 1800, role: "user", content: "Kept1" }, { ts: 1900, role: "assistant", content: "Kept2" }, @@ -407,9 +407,9 @@ describe("webviewMessageHandler delete functionality", () => { // Summary and regular message share timestamp (edge case) getCurrentTaskMock.apiConversationHistory = [ { ts: 900, role: "user", content: "Previous message" }, - { ts: sharedTs, role: "assistant", content: "Summary", isSummary: true, condenseId: "abc" }, - { ts: sharedTs, role: "user", content: "First kept message" }, - { ts: 1100, role: "assistant", content: "Response" }, + { ts: sharedTs, role: "user", content: "Summary", isSummary: true, condenseId: "abc" }, + { ts: sharedTs, role: "assistant", content: "First kept message" }, + { ts: 1100, role: "user", content: "Response" }, ] // Delete at shared timestamp - MessageManager uses ts < cutoffTs, so ALL @@ -450,10 +450,10 @@ describe("webviewMessageHandler delete functionality", () => { { ts: 100, role: "user", content: "Task start", condenseParent: condenseId }, { ts: 200, role: "assistant", content: "Response 1", condenseParent: condenseId }, // Summary timestamp is BEFORE the kept messages (this is the bug scenario) - { ts: 299, role: "assistant", content: "Summary text", isSummary: true, condenseId }, - { ts: 300, role: "user", content: "Message to delete this and after" }, - { ts: 400, role: "assistant", content: "Response 2" }, - { ts: 600, role: "user", content: "Post-condense message" }, + { ts: 299, role: "user", content: "Summary text", isSummary: true, condenseId }, + { ts: 300, role: "assistant", content: "Message to delete this and after" }, + { ts: 400, role: "user", content: "Response 2" }, + { ts: 600, role: "assistant", content: "Post-condense message" }, ] // Delete at ts=300 - this removes condense_context (ts=500), so Summary should be removed too @@ -510,30 +510,30 @@ describe("webviewMessageHandler delete functionality", () => { // First summary (also tagged with condenseId2 from second condense) { ts: 799, - role: "assistant", + role: "user", content: "First summary", isSummary: true, condenseId: condenseId1, condenseParent: condenseId2, }, - { ts: 900, role: "user", content: "After first condense", condenseParent: condenseId2 }, + { ts: 900, role: "assistant", content: "After first condense", condenseParent: condenseId2 }, { ts: 1000, - role: "assistant", + role: "user", content: "Response after 1st condense", condenseParent: condenseId2, }, - { ts: 1100, role: "user", content: "Message to delete this and after" }, + { ts: 1100, role: "assistant", content: "Message to delete this and after" }, // Second summary (timestamp is BEFORE the messages it summarized for sort purposes) { ts: 1799, - role: "assistant", + role: "user", content: "Second summary", isSummary: true, condenseId: condenseId2, }, - { ts: 1900, role: "user", content: "Post second condense" }, - { ts: 2000, role: "assistant", content: "Final response" }, + { ts: 1900, role: "assistant", content: "Post second condense" }, + { ts: 2000, role: "user", content: "Final response" }, ] // Delete at ts=1100 - this removes second condense_context (ts=1800) but keeps first (ts=800)