@@ -191,6 +191,8 @@ import {
191191 MAX_OVERALL_CHARACTERS ,
192192 FSREAD_MEMORY_BANK_MAX_PER_FILE ,
193193 FSREAD_MEMORY_BANK_MAX_TOTAL ,
194+ MID_LOOP_COMPACTION_HANDOFF_PROMPT ,
195+ COMPACTION_PROMPT ,
194196} from './constants/constants'
195197import {
196198 AgenticChatError ,
@@ -943,6 +945,11 @@ export class AgenticChatController implements ChatHandlers {
943945
944946 const compactIds = session . getAllDeferredCompactMessageIds ( )
945947 await this . #invalidateCompactCommand( params . tabId , compactIds )
948+ // Set compactionDeclined flag if there were pending compaction requests
949+ // This prevents endless compaction warning loops when user declines compaction once
950+ if ( compactIds . length > 0 ) {
951+ session . compactionDeclined = true
952+ }
946953 session . rejectAllDeferredToolExecutions ( new ToolApprovalException ( 'Command ignored: new prompt' , false ) )
947954 await this . #invalidateAllShellCommands( params . tabId , session )
948955
@@ -978,6 +985,10 @@ export class AgenticChatController implements ChatHandlers {
978985 session . abortRequest ( )
979986 const compactIds = session . getAllDeferredCompactMessageIds ( )
980987 await this . #invalidateCompactCommand( params . tabId , compactIds )
988+ // Set compactionDeclined flag if there were pending compaction requests
989+ if ( compactIds . length > 0 ) {
990+ session . compactionDeclined = true
991+ }
981992 void this . #invalidateAllShellCommands( params . tabId , session )
982993 session . rejectAllDeferredToolExecutions ( new CancellationError ( 'user' ) )
983994
@@ -1087,7 +1098,7 @@ export class AgenticChatController implements ChatHandlers {
10871098 }
10881099
10891100 // Result Handling - This happens only once
1090- return await this . #handleFinalResult(
1101+ const result = await this . #handleFinalResult(
10911102 finalResult ,
10921103 session ,
10931104 params . tabId ,
@@ -1096,6 +1107,13 @@ export class AgenticChatController implements ChatHandlers {
10961107 isNewConversation ,
10971108 chatResultStream
10981109 )
1110+
1111+ // Reset compactionDeclined flag after successful completion
1112+ if ( session . compactionDeclined ) {
1113+ session . compactionDeclined = false
1114+ }
1115+
1116+ return result
10991117 } catch ( err ) {
11001118 // HACK: the chat-client needs to have a partial event with the associated messageId sent before it can accept the final result.
11011119 // Without this, the `working` indicator never goes away.
@@ -1167,25 +1185,29 @@ export class AgenticChatController implements ChatHandlers {
11671185 /**
11681186 * Prepares the initial request input for the chat prompt
11691187 */
1170- #getCompactionRequestInput( session : ChatSessionService ) : ChatCommandInput {
1188+ #getCompactionRequestInput( session : ChatSessionService , toolResults ?: any [ ] ) : ChatCommandInput {
11711189 this . #debug( 'Preparing compaction request input' )
11721190 // Get profileArn from the service manager if available
11731191 const profileArn = this . #serviceManager?. getActiveProfileArn ( )
11741192 const requestInput = this . #triggerContext. getCompactionChatCommandInput (
11751193 profileArn ,
11761194 this . #getTools( session ) ,
11771195 session . modelId ,
1178- this . #origin
1196+ this . #origin,
1197+ toolResults
11791198 )
11801199 return requestInput
11811200 }
11821201
11831202 /**
11841203 * Runs the compaction, making requests and processing tool uses until completion
11851204 */
1186- #shouldCompact( currentRequestCount : number ) : boolean {
1187- if ( currentRequestCount > COMPACTION_CHARACTER_THRESHOLD ) {
1188- this . #debug( `Current request total character count is: ${ currentRequestCount } , prompting user to compact` )
1205+ #shouldCompact( currentRequestCount : number , session : ChatSessionService ) : boolean {
1206+ const EFFECTIVE_COMPACTION_THRESHOLD = COMPACTION_CHARACTER_THRESHOLD - COMPACTION_PROMPT . length
1207+ if ( currentRequestCount > EFFECTIVE_COMPACTION_THRESHOLD && ! session . compactionDeclined ) {
1208+ this . #debug(
1209+ `Current request total character count is: ${ currentRequestCount } , prompting user to compact (threshold: ${ EFFECTIVE_COMPACTION_THRESHOLD } )`
1210+ )
11891211 return true
11901212 } else {
11911213 return false
@@ -1368,6 +1390,10 @@ export class AgenticChatController implements ChatHandlers {
13681390 let currentRequestCount = 0
13691391 const pinnedContext = additionalContext ?. filter ( item => item . pinned )
13701392
1393+ // Store initial non-empty prompt for compaction handoff
1394+ const initialPrompt =
1395+ initialRequestInput . conversationState ?. currentMessage ?. userInputMessage ?. content ?. trim ( ) || ''
1396+
13711397 metric . recordStart ( )
13721398 this . logSystemInformation ( )
13731399 while ( true ) {
@@ -1432,6 +1458,63 @@ export class AgenticChatController implements ChatHandlers {
14321458 this . #llmRequestStartTime = Date . now ( )
14331459 // Phase 3: Request Execution
14341460 currentRequestInput = sanitizeRequestInput ( currentRequestInput )
1461+
1462+ if ( this . #shouldCompact( currentRequestCount , session ) ) {
1463+ this . #features. logging . info (
1464+ `Entering mid-loop compaction at iteration ${ iterationCount } with ${ currentRequestCount } characters`
1465+ )
1466+ this . #telemetryController. emitMidLoopCompaction (
1467+ currentRequestCount ,
1468+ iterationCount ,
1469+ this . #features. runtime . serverInfo . version ?? ''
1470+ )
1471+ const messageId = this . #getMessageIdForCompact( uuid ( ) )
1472+ const confirmationResult = this . #processCompactConfirmation( messageId , currentRequestCount )
1473+ const cachedButtonBlockId = await chatResultStream . writeResultBlock ( confirmationResult )
1474+ await this . waitForCompactApproval ( messageId , chatResultStream , cachedButtonBlockId , session )
1475+
1476+ // Run compaction
1477+ const toolResults =
1478+ currentRequestInput . conversationState ?. currentMessage ?. userInputMessage ?. userInputMessageContext
1479+ ?. toolResults || [ ]
1480+ const compactionRequestInput = this . #getCompactionRequestInput( session , toolResults )
1481+ const compactionResult = await this . #runCompaction(
1482+ compactionRequestInput ,
1483+ session ,
1484+ metric ,
1485+ chatResultStream ,
1486+ tabId ,
1487+ promptId ,
1488+ CompactHistoryActionType . Nudge ,
1489+ session . conversationId ,
1490+ token ,
1491+ documentReference
1492+ )
1493+
1494+ if ( ! compactionResult . success ) {
1495+ this . #features. logging . error ( `Compaction failed: ${ compactionResult . error } ` )
1496+ return compactionResult
1497+ }
1498+
1499+ // Show compaction summary to user before continuing
1500+ await chatResultStream . writeResultBlock ( {
1501+ type : 'answer' ,
1502+ body :
1503+ ( compactionResult . data ?. chatResult . body || '' ) +
1504+ '\n\nConversation history has been compacted successfully!' ,
1505+ messageId : uuid ( ) ,
1506+ } )
1507+
1508+ currentRequestInput = this . #updateRequestInputWithToolResults(
1509+ currentRequestInput ,
1510+ [ ] ,
1511+ MID_LOOP_COMPACTION_HANDOFF_PROMPT + initialPrompt
1512+ )
1513+ shouldDisplayMessage = false
1514+ this . #features. logging . info ( `Completed mid-loop compaction, restarting loop with handoff prompt` )
1515+ continue
1516+ }
1517+
14351518 // Note: these logs are very noisy, but contain information redacted on the backend.
14361519 this . #debug(
14371520 `generateAssistantResponse/SendMessage Request: ${ JSON . stringify ( currentRequestInput , this . #imageReplacer, 2 ) } `
@@ -1660,7 +1743,7 @@ export class AgenticChatController implements ChatHandlers {
16601743 currentRequestInput = this . #updateRequestInputWithToolResults( currentRequestInput , toolResults , content )
16611744 }
16621745
1663- if ( this . #shouldCompact( currentRequestCount ) ) {
1746+ if ( this . #shouldCompact( currentRequestCount , session ) ) {
16641747 this . #telemetryController. emitCompactNudge (
16651748 currentRequestCount ,
16661749 this . #features. runtime . serverInfo . version ?? ''
0 commit comments