Explorar el Código

fix: prevent nested condensing from including previously-condensed content (#10985)

Hannes Rudolph hace 2 semanas
padre
commit
2f92cb7a8d

+ 1 - 16
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])
-		})
 	})
 })

+ 13 - 15
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 },
 		])
 	})
 

+ 211 - 0
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)
+		})
+	})
+})

+ 23 - 24
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)

+ 9 - 25
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)
 }
 
 /**

+ 8 - 8
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,

+ 19 - 19
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)