fix: process queued user messages during endless loop recovery (Issue #10814)

When the model gets stuck in a repetitive loop (repeating the same tool call
or not using tools), user messages sent during this time are queued but never
processed because the model keeps receiving the same error feedback.

This fix adds queue processing at two critical points:

1. In Task.ts when consecutiveNoToolUseCount >= 2: Check for queued messages
   and inject them as user feedback to break the loop.

2. In presentAssistantMessage.ts when tool repetition is detected: Check for
   queued messages before asking the user, allowing previously queued messages
   to provide the intervention needed.

Both fixes reset the mistake counters when user guidance is received, giving
the model a fresh start with the user-provided direction.

Fixes #10814
This commit is contained in:
Roo Code 2026-01-18 02:54:30 +00:00
parent 802b40a790
commit 3da18c7712
2 changed files with 59 additions and 12 deletions

View file

@ -824,24 +824,48 @@ export async function presentAssistantMessage(cline: Task) {
// If execution is not allowed, notify user and break.
if (!repetitionCheck.allowExecution && repetitionCheck.askUser) {
// Handle repetition similar to mistake_limit_reached pattern.
const { response, text, images } = await cline.ask(
repetitionCheck.askUser.messageKey as ClineAsk,
repetitionCheck.askUser.messageDetail.replace("{toolName}", block.name),
)
let feedbackText: string | undefined
let feedbackImages: string[] | undefined
if (response === "messageResponse") {
// Add user feedback to userContent.
// CRITICAL FIX (GitHub #10814): Check for queued user messages first.
// When auto-approve is enabled and the model gets stuck in a loop,
// user messages are queued but never processed because the model
// keeps repeating the same tool call. By checking the queue here,
// we allow user intervention to break the loop.
if (!cline.messageQueueService.isEmpty()) {
const queuedMessage = cline.messageQueueService.dequeueMessage()
if (queuedMessage) {
feedbackText = queuedMessage.text
feedbackImages = queuedMessage.images
// Show the user's feedback in chat
await cline.say("user_feedback", feedbackText, feedbackImages)
}
}
// If no queued message, ask the user for feedback
if (!feedbackText) {
const { response, text, images } = await cline.ask(
repetitionCheck.askUser.messageKey as ClineAsk,
repetitionCheck.askUser.messageDetail.replace("{toolName}", block.name),
)
if (response === "messageResponse") {
feedbackText = text
feedbackImages = images
// Add user feedback to chat.
await cline.say("user_feedback", text, images)
}
}
// If we have user feedback (from queue or ask), add it to userContent
if (feedbackText) {
cline.userMessageContent.push(
{
type: "text" as const,
text: `Tool repetition limit reached. User feedback: ${text}`,
text: `Tool repetition limit reached. User feedback: ${feedbackText}`,
},
...formatResponse.imageBlocks(images),
...formatResponse.imageBlocks(feedbackImages),
)
// Add user feedback to chat.
await cline.say("user_feedback", text, images)
}
// Track tool repetition in telemetry via PostHog exception tracking and event.

View file

@ -3540,6 +3540,29 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
await this.say("error", "MODEL_NO_TOOLS_USED")
// Only count toward mistake limit after second consecutive failure
this.consecutiveMistakeCount++
// CRITICAL: Check for queued user messages when the model appears stuck.
// This allows user intervention to break out of endless loops where the model
// keeps trying the same approach without using tools (GitHub issue #10814).
if (!this.messageQueueService.isEmpty()) {
const queuedMessage = this.messageQueueService.dequeueMessage()
if (queuedMessage) {
// Add user's message as feedback to break the loop
await this.say("user_feedback", queuedMessage.text, queuedMessage.images)
this.userMessageContent.push({
type: "text",
text: formatResponse.tooManyMistakes(queuedMessage.text),
})
if (queuedMessage.images && queuedMessage.images.length > 0) {
this.userMessageContent.push(
...formatResponse.imageBlocks(queuedMessage.images),
)
}
// Reset mistake counter since user provided guidance
this.consecutiveMistakeCount = 0
this.consecutiveNoToolUseCount = 0
}
}
}
// Use the task's locked protocol for consistent behavior