mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix(condense): preserve lineage across successive condenses and add UI-first rewind hygiene
- Tag prior summaries within the current window with condenseParent (preserve original condenseId/isSummary) - Propagate condenseParent to the previous UI condense_context for lineage continuity - Add rewind-aware hygiene (uiOnly) so deletes/rewinds restore API history to exact pre-condense state at the chosen point - Keep tests green (condense + task suites)
This commit is contained in:
parent
af75083b8a
commit
5b925cf751
3 changed files with 96 additions and 24 deletions
|
|
@ -196,16 +196,12 @@ export async function summarizeConversation(
|
|||
condenseId: condenseId,
|
||||
}
|
||||
|
||||
// Tag middle messages from the full middle span, but only set condenseParent
|
||||
// for those that were actually part of the current summarization window and lack a tag.
|
||||
const windowTs = new Set(
|
||||
messagesToSummarize
|
||||
.slice(1) // skip the preserved first
|
||||
.map((m) => m.ts)
|
||||
.filter((ts): ts is number => typeof ts === "number"),
|
||||
)
|
||||
// Tag middle messages from the full middle span, including any previous summaries
|
||||
// that were part of this summarization window. Preserve existing condenseId/isSummary.
|
||||
const windowTs = new Set(messagesToSummarize.map((m) => m.ts).filter((ts): ts is number => typeof ts === "number"))
|
||||
const middleMessages = messages.slice(1, -N_MESSAGES_TO_KEEP).map((msg) => {
|
||||
if (!msg.isSummary && typeof msg.ts === "number" && windowTs.has(msg.ts)) {
|
||||
if (typeof msg.ts === "number" && windowTs.has(msg.ts)) {
|
||||
// Do not alter isSummary or condenseId on prior summaries; only add condenseParent if missing
|
||||
return { ...msg, condenseParent: msg.condenseParent ?? condenseId }
|
||||
}
|
||||
return msg
|
||||
|
|
|
|||
|
|
@ -1040,6 +1040,29 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
// Set flag to skip previous_response_id on the next API call after manual condense
|
||||
this.skipPrevResponseIdOnce = true
|
||||
|
||||
// If there was a previous condense_context in the UI, tag it with condenseParent = newCondenseId
|
||||
try {
|
||||
const newCondenseId = messages.find((m) => m.isSummary && m.condenseId)?.condenseId
|
||||
if (newCondenseId) {
|
||||
const lastCondenseIdx = findLastIndex(
|
||||
this.clineMessages,
|
||||
(m) => m.type === "say" && m.say === "condense_context",
|
||||
)
|
||||
if (lastCondenseIdx !== -1) {
|
||||
const lastCondenseMsg = this.clineMessages[lastCondenseIdx] as ClineMessage &
|
||||
ClineMessageWithMetadata
|
||||
lastCondenseMsg.metadata = lastCondenseMsg.metadata ?? {}
|
||||
if (!lastCondenseMsg.metadata.condenseParent) {
|
||||
lastCondenseMsg.metadata.condenseParent = newCondenseId
|
||||
await this.saveClineMessages()
|
||||
await this.updateClineMessage(lastCondenseMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// non-fatal; UI metadata tagging is best-effort
|
||||
}
|
||||
|
||||
const contextCondense: ContextCondense = { summary, cost, newContextTokens, prevContextTokens }
|
||||
await this.say(
|
||||
"condense_context",
|
||||
|
|
@ -2627,6 +2650,39 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
// send previous_response_id so the request reflects the fresh condensed context.
|
||||
this.skipPrevResponseIdOnce = true
|
||||
|
||||
// Determine the condenseId of the newly created summary in API history
|
||||
let newCondenseId: string | undefined
|
||||
try {
|
||||
const lastSummary = [...this.apiConversationHistory]
|
||||
.reverse()
|
||||
.find((m) => m.isSummary && m.condenseId)
|
||||
newCondenseId = lastSummary?.condenseId
|
||||
} catch {
|
||||
// non-fatal
|
||||
}
|
||||
|
||||
// Tag the previous UI condense_context (if any) with condenseParent to maintain lineage
|
||||
try {
|
||||
if (newCondenseId) {
|
||||
const lastCondenseIdx = findLastIndex(
|
||||
this.clineMessages,
|
||||
(m) => m.type === "say" && m.say === "condense_context",
|
||||
)
|
||||
if (lastCondenseIdx !== -1) {
|
||||
const lastCondenseMsg = this.clineMessages[lastCondenseIdx] as ClineMessage &
|
||||
ClineMessageWithMetadata
|
||||
lastCondenseMsg.metadata = lastCondenseMsg.metadata ?? {}
|
||||
if (!lastCondenseMsg.metadata.condenseParent) {
|
||||
lastCondenseMsg.metadata.condenseParent = newCondenseId
|
||||
await this.saveClineMessages()
|
||||
await this.updateClineMessage(lastCondenseMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
|
||||
const { summary, cost, prevContextTokens, newContextTokens = 0 } = truncateResult
|
||||
const contextCondense: ContextCondense = { summary, cost, newContextTokens, prevContextTokens }
|
||||
await this.say(
|
||||
|
|
@ -2636,7 +2692,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
false /* partial */,
|
||||
undefined /* checkpoint */,
|
||||
undefined /* progressStatus */,
|
||||
{ isNonInteractive: true } /* options */,
|
||||
{
|
||||
isNonInteractive: true,
|
||||
metadata: newCondenseId ? { condenseId: newCondenseId } : undefined,
|
||||
} /* options */,
|
||||
contextCondense,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,33 +113,51 @@ export const webviewMessageHandler = async (
|
|||
)
|
||||
}
|
||||
|
||||
// Perform hygiene: clean up orphaned condenseParent references
|
||||
await performCondenseHygiene(currentCline)
|
||||
// Perform hygiene: UI is source-of-truth during rewinds (purge API summaries whose UI counterparts were removed)
|
||||
await performCondenseHygiene(currentCline, { uiOnly: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up orphaned condenseParent references after truncation
|
||||
*/
|
||||
const performCondenseHygiene = async (currentCline: any) => {
|
||||
// Find all active condenseIds (from remaining summary messages)
|
||||
const performCondenseHygiene = async (currentCline: any, opts?: { uiOnly?: boolean }) => {
|
||||
// Build active condenseIds. If uiOnly, treat UI as source-of-truth (used during rewind/delete).
|
||||
// Otherwise, include API summaries too (general hygiene).
|
||||
const activeCondenseIds = new Set<string>()
|
||||
|
||||
// Check API conversation history for active summaries
|
||||
currentCline.apiConversationHistory.forEach((msg: ApiMessage) => {
|
||||
if (msg.isSummary && msg.condenseId) {
|
||||
activeCondenseIds.add(msg.condenseId)
|
||||
}
|
||||
})
|
||||
|
||||
// Check UI messages for active summaries
|
||||
// Always include UI-derived condenseIds
|
||||
currentCline.clineMessages.forEach((msg: any) => {
|
||||
if (msg.say === "condense_context" && msg.metadata?.condenseId) {
|
||||
activeCondenseIds.add(msg.metadata.condenseId)
|
||||
}
|
||||
})
|
||||
|
||||
// Clean up orphaned condenseParent references in API history
|
||||
// Optionally include API summaries that remain in history (non-delete hygiene)
|
||||
if (!opts?.uiOnly) {
|
||||
currentCline.apiConversationHistory.forEach((msg: ApiMessage) => {
|
||||
if (msg.isSummary && msg.condenseId) {
|
||||
activeCondenseIds.add(msg.condenseId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let apiHistoryModified = false
|
||||
let uiMessagesModified = false
|
||||
|
||||
// Purge API summaries that are not represented by UI when uiOnly=true (rewind to pre-condense state).
|
||||
// In general hygiene (uiOnly=false), we only remove summaries with condenseIds not present anywhere.
|
||||
const beforeLen = currentCline.apiConversationHistory.length
|
||||
currentCline.apiConversationHistory = currentCline.apiConversationHistory.filter((msg: ApiMessage) => {
|
||||
if (msg.isSummary && msg.condenseId && !activeCondenseIds.has(msg.condenseId)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if (currentCline.apiConversationHistory.length !== beforeLen) {
|
||||
apiHistoryModified = true
|
||||
}
|
||||
|
||||
// Clean up orphaned condenseParent references in API history
|
||||
currentCline.apiConversationHistory.forEach((msg: ApiMessage) => {
|
||||
if (msg.condenseParent && !activeCondenseIds.has(msg.condenseParent)) {
|
||||
delete msg.condenseParent
|
||||
|
|
@ -148,7 +166,6 @@ export const webviewMessageHandler = async (
|
|||
})
|
||||
|
||||
// Clean up orphaned condenseParent references in UI messages
|
||||
let uiMessagesModified = false
|
||||
currentCline.clineMessages.forEach((msg: any) => {
|
||||
if (msg.metadata?.condenseParent && !activeCondenseIds.has(msg.metadata.condenseParent)) {
|
||||
delete msg.metadata.condenseParent
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue