mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
fix: prevent nested condensing from including previously-condensed content (#10985)
This commit is contained in:
parent
dd245cc40c
commit
2f92cb7a8d
7 changed files with 284 additions and 107 deletions
|
|
@ -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])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
src/core/condense/__tests__/nested-condense.spec.ts
Normal file
211
src/core/condense/__tests__/nested-condense.spec.ts
Normal file
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue