@@ -323,6 +323,7 @@ export namespace SessionPrompt {
323323 // altimate_change start — plan refinement tracking
324324 let planRevisionCount = 0
325325 let planHasWritten = false
326+ let planLastUserMsgId : string | undefined
326327 // altimate_change end
327328 let emergencySessionEndFired = false
328329 // altimate_change start — quality signal, tool chain, error fingerprint tracking
@@ -637,10 +638,12 @@ export namespace SessionPrompt {
637638 const planPath = Session . plan ( session )
638639 planHasWritten = await Filesystem . exists ( planPath )
639640 }
640- // If plan was already written and user sent a new message, this is a refinement
641- if ( planHasWritten && step > 1 ) {
642- // Detect approval phrases in the last user message text
643- const lastUserMsg = msgs . findLast ( ( m ) => m . info . role === "user" )
641+ // If plan was already written and user sent a new message, this is a refinement.
642+ // Only count once per user message (not on internal loop iterations).
643+ const lastUserMsg = msgs . findLast ( ( m ) => m . info . role === "user" )
644+ const currentUserMsgId = lastUserMsg ?. info . id
645+ if ( planHasWritten && step > 1 && currentUserMsgId && currentUserMsgId !== planLastUserMsgId ) {
646+ planLastUserMsgId = currentUserMsgId
644647 const userText = lastUserMsg ?. parts
645648 . filter ( ( p ) : p is MessageV2 . TextPart => p . type === "text" && ! ( "synthetic" in p && p . synthetic ) )
646649 . map ( ( p ) => p . text . toLowerCase ( ) )
@@ -678,7 +681,7 @@ export namespace SessionPrompt {
678681 const refinementQualifiers = [ " but " , " however " , " except " , " change " , " modify " , " update " , " instead " , " although " , " with the following" , " with these" ]
679682 const hasRefinementQualifier = refinementQualifiers . some ( ( q ) => userText . includes ( q ) )
680683
681- const rejectionPhrases = [ "don't" , "stop" , "reject" , "not good" , "undo" , "abort" , "start over" , "wrong" ]
684+ const rejectionPhrases = [ "don't" , "stop" , "reject" , "not good" , "not approve" , "not approved" , "disapprove" , " undo", "abort" , "start over" , "wrong" ]
682685 // "no" as a standalone word to avoid matching "know", "notion", etc.
683686 const rejectionWords = [ "no" ]
684687 const approvalPhrases = [ "looks good" , "proceed" , "approved" , "approve" , "lgtm" , "go ahead" , "ship it" , "yes" , "perfect" ]
@@ -689,7 +692,12 @@ export namespace SessionPrompt {
689692 return regex . test ( userText )
690693 } )
691694 const isRejection = isRejectionPhrase || isRejectionWord
692- const isApproval = ! isRejection && ! hasRefinementQualifier && approvalPhrases . some ( ( phrase ) => userText . includes ( phrase ) )
695+ // Use word-boundary matching for approval phrases to avoid false positives
696+ // e.g. "this doesn't look good" should NOT match "looks good"
697+ const isApproval = ! isRejection && ! hasRefinementQualifier && approvalPhrases . some ( ( phrase ) => {
698+ const regex = new RegExp ( `\\b${ phrase . replace ( / \s + / g, "\\s+" ) } \\b` , "i" )
699+ return regex . test ( userText )
700+ } )
693701 const action = isRejection ? "reject" : isApproval ? "approve" : "refine"
694702 Telemetry . track ( {
695703 type : "plan_revision" ,
0 commit comments