@@ -109,7 +109,7 @@ import {
109109 checkpointDiff ,
110110} from "../checkpoints"
111111import { processUserContentMentions } from "../mentions/processUserContentMentions"
112- import { getMessagesSinceLastSummary , summarizeConversation } from "../condense"
112+ import { getMessagesSinceLastSummary , summarizeConversation , SummarizeResponse } from "../condense"
113113import { Gpt5Metadata , ClineMessageWithMetadata } from "./types"
114114import { MessageQueueService } from "../message-queue/MessageQueueService"
115115
@@ -1007,13 +1007,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
10071007
10081008 const { contextTokens : prevContextTokens } = this . getTokenUsage ( )
10091009
1010- const {
1011- messages,
1012- summary,
1013- cost,
1014- newContextTokens = 0 ,
1015- error,
1016- } = await summarizeConversation (
1010+ const result = await summarizeConversation (
10171011 this . apiConversationHistory ,
10181012 this . api , // Main API handler (fallback)
10191013 systemPrompt , // Default summarization prompt (fallback)
@@ -1023,10 +1017,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
10231017 customCondensingPrompt , // User's custom prompt
10241018 condensingApiHandler , // Specific handler for condensing
10251019 )
1026- if ( error ) {
1020+
1021+ if ( result . error ) {
10271022 this . say (
10281023 "condense_context_error" ,
1029- error ,
1024+ result . error ,
10301025 undefined /* images */ ,
10311026 false /* partial */ ,
10321027 undefined /* checkpoint */ ,
@@ -1035,11 +1030,16 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
10351030 )
10361031 return
10371032 }
1038- await this . overwriteApiConversationHistory ( messages )
1033+
1034+ // Merge condense metadata into API and UI histories (mark children and replace summary)
1035+ const { condenseId = `c_${ Date . now ( ) . toString ( 36 ) } _${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` } =
1036+ result as SummarizeResponse
1037+ await this . applyCondenseMetadataToHistories ( result )
10391038
10401039 // Set flag to skip previous_response_id on the next API call after manual condense
10411040 this . skipPrevResponseIdOnce = true
10421041
1042+ const { summary, cost, newContextTokens = 0 } = result as SummarizeResponse
10431043 const contextCondense : ContextCondense = { summary, cost, newContextTokens, prevContextTokens }
10441044 await this . say (
10451045 "condense_context" ,
@@ -1051,6 +1051,18 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
10511051 { isNonInteractive : true } /* options */ ,
10521052 contextCondense ,
10531053 )
1054+
1055+ // Tag the condense_context UI message with condenseId for cross-linking
1056+ try {
1057+ const idx = findLastIndex ( this . clineMessages , ( m ) => m . type === "say" && m . say === "condense_context" )
1058+ if ( idx !== - 1 ) {
1059+ ; ( this . clineMessages [ idx ] as any ) . condenseId = condenseId
1060+ await this . saveClineMessages ( )
1061+ await this . updateClineMessage ( this . clineMessages [ idx ] )
1062+ }
1063+ } catch {
1064+ // non-fatal
1065+ }
10541066 }
10551067
10561068 async say (
@@ -2497,11 +2509,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
24972509 } )
24982510
24992511 if ( truncateResult . messages !== this . apiConversationHistory ) {
2500- await this . overwriteApiConversationHistory ( truncateResult . messages )
2512+ await this . applyCondenseMetadataToHistories ( truncateResult )
25012513 }
25022514
25032515 if ( truncateResult . summary ) {
2504- const { summary, cost, prevContextTokens, newContextTokens = 0 } = truncateResult
2516+ const { summary, cost, prevContextTokens, newContextTokens = 0 , condenseId } = truncateResult
25052517 const contextCondense : ContextCondense = { summary, cost, newContextTokens, prevContextTokens }
25062518 await this . say (
25072519 "condense_context" ,
@@ -2513,6 +2525,23 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
25132525 { isNonInteractive : true } /* options */ ,
25142526 contextCondense ,
25152527 )
2528+
2529+ // Tag the condense_context UI message with condenseId for cross-linking
2530+ try {
2531+ if ( condenseId ) {
2532+ const idx = findLastIndex (
2533+ this . clineMessages ,
2534+ ( m ) => m . type === "say" && m . say === "condense_context" ,
2535+ )
2536+ if ( idx !== - 1 ) {
2537+ ; ( this . clineMessages [ idx ] as any ) . condenseId = condenseId
2538+ await this . saveClineMessages ( )
2539+ await this . updateClineMessage ( this . clineMessages [ idx ] )
2540+ }
2541+ }
2542+ } catch {
2543+ // non-fatal
2544+ }
25162545 }
25172546 }
25182547
@@ -2613,7 +2642,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
26132642 currentProfileId,
26142643 } )
26152644 if ( truncateResult . messages !== this . apiConversationHistory ) {
2616- await this . overwriteApiConversationHistory ( truncateResult . messages )
2645+ await this . applyCondenseMetadataToHistories ( truncateResult )
26172646 }
26182647 if ( truncateResult . error ) {
26192648 await this . say ( "condense_context_error" , truncateResult . error )
@@ -2622,7 +2651,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
26222651 // send previous_response_id so the request reflects the fresh condensed context.
26232652 this . skipPrevResponseIdOnce = true
26242653
2625- const { summary, cost, prevContextTokens, newContextTokens = 0 } = truncateResult
2654+ const { summary, cost, prevContextTokens, newContextTokens = 0 , condenseId } = truncateResult
26262655 const contextCondense : ContextCondense = { summary, cost, newContextTokens, prevContextTokens }
26272656 await this . say (
26282657 "condense_context" ,
@@ -2634,10 +2663,29 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
26342663 { isNonInteractive : true } /* options */ ,
26352664 contextCondense ,
26362665 )
2666+
2667+ // Tag the condense_context UI message with condenseId for cross-linking
2668+ try {
2669+ if ( condenseId ) {
2670+ const idx = findLastIndex (
2671+ this . clineMessages ,
2672+ ( m ) => m . type === "say" && m . say === "condense_context" ,
2673+ )
2674+ if ( idx !== - 1 ) {
2675+ ; ( this . clineMessages [ idx ] as any ) . condenseId = condenseId
2676+ await this . saveClineMessages ( )
2677+ await this . updateClineMessage ( this . clineMessages [ idx ] )
2678+ }
2679+ }
2680+ } catch {
2681+ // non-fatal
2682+ }
26372683 }
26382684 }
26392685
2640- const messagesSinceLastSummary = getMessagesSinceLastSummary ( this . apiConversationHistory )
2686+ // Assemble API history excluding condensed children
2687+ const filteredForApi = this . apiConversationHistory . filter ( ( m : any ) => ! m ?. condenseParent )
2688+ const messagesSinceLastSummary = getMessagesSinceLastSummary ( filteredForApi )
26412689 let cleanConversationHistory = maybeRemoveImageBlocks ( messagesSinceLastSummary , this . api ) . map (
26422690 ( { role, content } ) => ( { role, content } ) ,
26432691 )
@@ -2888,6 +2936,82 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
28882936 }
28892937 }
28902938
2939+ // Condense helpers
2940+
2941+ /**
2942+ * Apply condense metadata to both API and UI histories:
2943+ * - Replace the first kept tail message with the summary (parent) and set its condenseId
2944+ * - Mark all prior messages (excluding the original first message) with condenseParent = condenseId
2945+ * - Persist changes to disk
2946+ */
2947+ private async applyCondenseMetadataToHistories (
2948+ result : SummarizeResponse ,
2949+ ) : Promise < { condenseId : string ; thresholdTs ?: number } > {
2950+ try {
2951+ const condenseId =
2952+ result . condenseId ?? `c_${ Date . now ( ) . toString ( 36 ) } _${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } `
2953+ const summaryMsg = ( result . messages || [ ] ) . find ( ( m ) => ( m as any ) . isSummary ) as ApiMessage | undefined
2954+ const thresholdTs = summaryMsg ?. ts
2955+ const firstApiTs = this . apiConversationHistory [ 0 ] ?. ts
2956+
2957+ // Build new API history by replacing the threshold message with the summary and marking children
2958+ let replacedSummary = false
2959+ let newApiHistory : ApiMessage [ ] = this . apiConversationHistory . map ( ( m ) => {
2960+ // Replace the first kept message with the new summary
2961+ if ( typeof thresholdTs === "number" && m . ts === thresholdTs && summaryMsg ) {
2962+ replacedSummary = true
2963+ return { ...summaryMsg , condenseId }
2964+ }
2965+ // Mark all messages before the threshold as condensed children, except the original first message
2966+ if ( typeof thresholdTs === "number" && typeof m . ts === "number" && m . ts < thresholdTs ) {
2967+ if ( typeof firstApiTs === "number" && m . ts === firstApiTs ) {
2968+ return m
2969+ }
2970+ if ( ! ( m as any ) . condenseParent ) {
2971+ return { ...m , condenseParent : condenseId }
2972+ }
2973+ }
2974+ return m
2975+ } )
2976+
2977+ // If no existing message matched the summary timestamp, insert summary just before the threshold boundary
2978+ if ( ! replacedSummary && summaryMsg && typeof thresholdTs === "number" ) {
2979+ let insertAt = newApiHistory . findIndex (
2980+ ( m ) => typeof m . ts === "number" && ( m . ts as number ) >= thresholdTs ,
2981+ )
2982+ if ( insertAt === - 1 ) insertAt = newApiHistory . length
2983+ newApiHistory = [
2984+ ...newApiHistory . slice ( 0 , insertAt ) ,
2985+ { ...summaryMsg , condenseId } ,
2986+ ...newApiHistory . slice ( insertAt ) ,
2987+ ]
2988+ }
2989+
2990+ this . apiConversationHistory = newApiHistory
2991+ await this . saveApiConversationHistory ( )
2992+
2993+ // Mark UI messages prior to the threshold as condensed children (exclude very first UI row)
2994+ if ( typeof thresholdTs === "number" ) {
2995+ let updated = false
2996+ for ( let i = 0 ; i < this . clineMessages . length ; i ++ ) {
2997+ const uiMsg = this . clineMessages [ i ] as any
2998+ if ( typeof uiMsg ?. ts === "number" && uiMsg . ts < thresholdTs && i > 0 && ! uiMsg . condenseParent ) {
2999+ uiMsg . condenseParent = condenseId
3000+ updated = true
3001+ }
3002+ }
3003+ if ( updated ) {
3004+ await this . saveClineMessages ( )
3005+ }
3006+ }
3007+
3008+ return { condenseId, thresholdTs }
3009+ } catch ( e ) {
3010+ console . error ( `[Task#${ this . taskId } ] Failed to apply condense metadata:` , e )
3011+ return { condenseId : `c_${ Date . now ( ) . toString ( 36 ) } ` , thresholdTs : undefined }
3012+ }
3013+ }
3014+
28913015 // Getters
28923016
28933017 public get taskStatus ( ) : TaskStatus {
0 commit comments