@@ -902,6 +902,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
902902 const { images, task, historyItem } = options
903903 let promise
904904
905+ instance . startIdleTelemetryCheck ( )
906+
905907 if ( images || task ) {
906908 promise = instance . startTask ( task , images )
907909 } else if ( historyItem ) {
@@ -1924,6 +1926,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
19241926 return Promise . resolve ( )
19251927 }
19261928 this . _started = true
1929+ this . startIdleTelemetryCheck ( )
19271930
19281931 const { task, images } = this . metadata
19291932
@@ -3689,22 +3692,17 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
36893692 // we need to remove that message before retrying to avoid having two consecutive
36903693 // user messages (which would cause tool_result validation errors).
36913694 const state = await this . providerRef . deref ( ) ?. getState ( )
3692- if ( this . apiConversationHistory . length > 0 ) {
3695+ // Only pop the user message that this iteration added. When
3696+ // shouldAddUserMessage is false (empty continuation, resumed history,
3697+ // or flushPendingToolResultsToHistory message) there is nothing to
3698+ // remove, and popping would corrupt history.
3699+ let removedCurrentUserMessage = false
3700+ if ( shouldAddUserMessage && this . apiConversationHistory . length > 0 ) {
36933701 const lastMessage = this . apiConversationHistory [ this . apiConversationHistory . length - 1 ]
36943702 if ( lastMessage . role === "user" ) {
3695- // Remove the last user message that we added earlier. Decrement
3696- // messageCounts.user to match -- both retry branches below mark
3697- // userMessageWasRemoved so the message (and its count) is restored
3698- // exactly once when the retry succeeds, keeping the total symmetric
3699- // regardless of how many empty-response cycles occur first.
3700- // Guard: only reverse a count this iteration actually added. The
3701- // popped message may predate this turn (resumed history, or a
3702- // message appended by flushPendingToolResultsToHistory, neither
3703- // of which incremented messageCounts.user).
37043703 this . apiConversationHistory . pop ( )
3705- if ( shouldAddUserMessage ) {
3706- this . messageCounts . user --
3707- }
3704+ this . messageCounts . user --
3705+ removedCurrentUserMessage = true
37083706 }
37093707 }
37103708
@@ -3727,13 +3725,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
37273725 break
37283726 }
37293727
3730- // Push the same content back onto the stack to retry, incrementing the retry attempt counter
3731- // Mark that user message was removed so it gets re-added on retry
3728+ // Push the same content back onto the stack to retry, incrementing the retry attempt counter.
3729+ // Only mark userMessageWasRemoved when we actually removed one -- the
3730+ // restore branch in shouldAddUserMessageToHistory must only fire once.
37323731 stack . push ( {
37333732 userContent : currentUserContent ,
37343733 includeFileDetails : false ,
37353734 retryAttempt : ( currentItem . retryAttempt ?? 0 ) + 1 ,
3736- userMessageWasRemoved : true ,
3735+ userMessageWasRemoved : removedCurrentUserMessage ,
37373736 } )
37383737
37393738 // Continue to retry the request
@@ -3748,31 +3747,27 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
37483747 if ( response === "yesButtonClicked" ) {
37493748 await this . say ( "api_req_retried" )
37503749
3751- // Push the same content back to retry. Mark that user message was
3752- // removed (same as the auto-retry path above) so it gets re-added --
3753- // and messageCounts.user re-incremented -- on the retried attempt;
3754- // otherwise shouldAddUserMessageToHistory sees retryAttempt > 0 with
3755- // userMessageWasRemoved unset and skips re-adding it entirely.
3750+ // Push the same content back to retry. Only mark userMessageWasRemoved
3751+ // when we actually removed one so the restore fires exactly once.
37563752 stack . push ( {
37573753 userContent : currentUserContent ,
37583754 includeFileDetails : false ,
37593755 retryAttempt : ( currentItem . retryAttempt ?? 0 ) + 1 ,
3760- userMessageWasRemoved : true ,
3756+ userMessageWasRemoved : removedCurrentUserMessage ,
37613757 } )
37623758
37633759 // Continue to retry the request
37643760 continue
37653761 } else {
3766- // User declined to retry
3767- // Re-add the user message we removed (see messageCounts.user-- above)
3768- // and increment messageCounts.user to match, same as the normal
3769- // add-to-history path -- otherwise this abandoned-task path
3770- // permanently undercounts by one.
3771- await this . addToApiConversationHistory ( {
3772- role : "user" ,
3773- content : currentUserContent ,
3774- } )
3775- this . messageCounts . user ++
3762+ // User declined to retry. Re-add the user message only if this
3763+ // iteration removed one, so the history and counter stay consistent.
3764+ if ( removedCurrentUserMessage ) {
3765+ await this . addToApiConversationHistory ( {
3766+ role : "user" ,
3767+ content : currentUserContent ,
3768+ } )
3769+ this . messageCounts . user ++
3770+ }
37763771
37773772 await this . say (
37783773 "error" ,
@@ -4781,6 +4776,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
47814776 }
47824777
47834778 startIdleTelemetryCheck ( ) : void {
4779+ if ( this . idleTelemetryCheckInterval !== undefined ) {
4780+ return
4781+ }
47844782 this . idleTelemetryCheckInterval = setInterval ( ( ) => {
47854783 // Measure idleness from the later of the last activity and the last flush.
47864784 // Using lastMessageTs alone would keep the condition true forever after the
0 commit comments