From 0711c6ff1515ec2b7d87d50cb20b67ce3d990572 Mon Sep 17 00:00:00 2001 From: ElecTwix Date: Sat, 25 Oct 2025 20:47:21 +0300 Subject: [PATCH 1/7] fix: add extensible loop detection for doom loop models (GLM-4.6, Grok Code) --- packages/opencode/src/session/prompt.ts | 33 +++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 6adeb6f7e0..21099f0666 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -913,6 +913,20 @@ export namespace SessionPrompt { let snapshot: string | undefined let blocked = false + const DOOM_LOOP_MODELS = { + "glm-4.6": `⚠️ Loop detected! You have been calling "%TOOL%" with the same parameters repeatedly. This is causing an infinite loop. Please STOP calling this tool and try a different approach or modify the parameters.`, + "grok-code": `⚠️ Loop detected! You're stuck repeating "%TOOL%" with identical inputs. Break this pattern and try a different strategy or change the parameters.`, + } as const + const LOOP_DETECTION_THRESHOLD = 3 + + const loopCounts = new Map() + const getLoopMessage = (modelId: string, toolName: string) => { + const template = DOOM_LOOP_MODELS[modelId as keyof typeof DOOM_LOOP_MODELS] + return template + ? template.replace("%TOOL%", toolName) + : `⚠️ Loop detected: Stop repeating "${toolName}" with the same parameters.` + } + async function createMessage(parentID: string) { const msg: MessageV2.Info = { id: Identifier.ascending("message"), @@ -1048,6 +1062,25 @@ export namespace SessionPrompt { case "tool-call": { const match = toolcalls[value.toolCallId] if (match) { + if (DOOM_LOOP_MODELS[input.model.id as keyof typeof DOOM_LOOP_MODELS]) { + const key = `${value.toolName}|${JSON.stringify(value.input)}` + const count = (loopCounts.get(key) || 0) + 1 + loopCounts.set(key, count) + + if (count >= LOOP_DETECTION_THRESHOLD) { + await Session.updatePart({ + id: Identifier.ascending("part"), + messageID: assistantMsg!.id, + sessionID: assistantMsg!.sessionID, + type: "text", + text: getLoopMessage(input.model.id, value.toolName), + }) + loopCounts.delete(key) + } else { + loopCounts.clear() + } + } + const part = await Session.updatePart({ ...match, tool: value.toolName, From b7e432ccd3997a25771ba603b2964be4af6ec6a0 Mon Sep 17 00:00:00 2001 From: ElecTwix Date: Tue, 28 Oct 2025 21:39:14 +0300 Subject: [PATCH 2/7] fix: replace model-specific doom loop detection with universal permission-based approach --- packages/opencode/src/session/prompt.ts | 55 +++++++++++-------------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 21099f0666..54eee213e3 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -913,20 +913,6 @@ export namespace SessionPrompt { let snapshot: string | undefined let blocked = false - const DOOM_LOOP_MODELS = { - "glm-4.6": `⚠️ Loop detected! You have been calling "%TOOL%" with the same parameters repeatedly. This is causing an infinite loop. Please STOP calling this tool and try a different approach or modify the parameters.`, - "grok-code": `⚠️ Loop detected! You're stuck repeating "%TOOL%" with identical inputs. Break this pattern and try a different strategy or change the parameters.`, - } as const - const LOOP_DETECTION_THRESHOLD = 3 - - const loopCounts = new Map() - const getLoopMessage = (modelId: string, toolName: string) => { - const template = DOOM_LOOP_MODELS[modelId as keyof typeof DOOM_LOOP_MODELS] - return template - ? template.replace("%TOOL%", toolName) - : `⚠️ Loop detected: Stop repeating "${toolName}" with the same parameters.` - } - async function createMessage(parentID: string) { const msg: MessageV2.Info = { id: Identifier.ascending("message"), @@ -1062,23 +1048,30 @@ export namespace SessionPrompt { case "tool-call": { const match = toolcalls[value.toolCallId] if (match) { - if (DOOM_LOOP_MODELS[input.model.id as keyof typeof DOOM_LOOP_MODELS]) { - const key = `${value.toolName}|${JSON.stringify(value.input)}` - const count = (loopCounts.get(key) || 0) + 1 - loopCounts.set(key, count) - - if (count >= LOOP_DETECTION_THRESHOLD) { - await Session.updatePart({ - id: Identifier.ascending("part"), - messageID: assistantMsg!.id, - sessionID: assistantMsg!.sessionID, - type: "text", - text: getLoopMessage(input.model.id, value.toolName), - }) - loopCounts.delete(key) - } else { - loopCounts.clear() - } + const currentParts = await Session.getParts(assistantMsg!.id) + const toolParts = currentParts.filter((p): p is MessageV2.ToolPart => p.type === "tool") + const recentToolParts = toolParts.slice(-3) + if ( + recentToolParts.length === 3 && + recentToolParts.every( + (p) => + p.tool === value.toolName && + p.state.status === "running" && + JSON.stringify(p.state.input) === JSON.stringify(value.input), + ) + ) { + await Permission.ask({ + type: "tool-call", + sessionID: assistantMsg!.sessionID, + messageID: assistantMsg!.id, + callID: value.toolCallId, + title: `⚠️ Potential doom loop detected: "${value.toolName}"`, + metadata: { + tool: value.toolName, + input: value.input, + reason: "doom-loop-detected", + }, + }) } const part = await Session.updatePart({ From 4db570f02218051c2980b1b1df1211437c798498 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 29 Oct 2025 17:40:22 -0500 Subject: [PATCH 3/7] fix --- packages/opencode/src/session/prompt.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 54eee213e3..8505213bb8 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1049,14 +1049,16 @@ export namespace SessionPrompt { const match = toolcalls[value.toolCallId] if (match) { const currentParts = await Session.getParts(assistantMsg!.id) - const toolParts = currentParts.filter((p): p is MessageV2.ToolPart => p.type === "tool") - const recentToolParts = toolParts.slice(-3) + const toolParts = currentParts.filter( + (p): p is MessageV2.ToolPart => p.type === "tool" && p.state.status !== "pending", + ) + const lastThree = toolParts.slice(-3) if ( - recentToolParts.length === 3 && - recentToolParts.every( + lastThree.length === 3 && + lastThree.every( (p) => p.tool === value.toolName && - p.state.status === "running" && + p.state.status !== "pending" && JSON.stringify(p.state.input) === JSON.stringify(value.input), ) ) { From 193b513b548f24ad92ad54fecc913c1e4eb8b01c Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 29 Oct 2025 17:49:45 -0500 Subject: [PATCH 4/7] cleanup --- packages/opencode/src/session/prompt.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 8505213bb8..7ad71b0502 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1048,7 +1048,7 @@ export namespace SessionPrompt { case "tool-call": { const match = toolcalls[value.toolCallId] if (match) { - const currentParts = await Session.getParts(assistantMsg!.id) + const currentParts = await Session.getParts(assistantMsg.id) const toolParts = currentParts.filter( (p): p is MessageV2.ToolPart => p.type === "tool" && p.state.status !== "pending", ) @@ -1063,9 +1063,9 @@ export namespace SessionPrompt { ) ) { await Permission.ask({ - type: "tool-call", - sessionID: assistantMsg!.sessionID, - messageID: assistantMsg!.id, + type: tool.name, + sessionID: assistantMsg.sessionID, + messageID: assistantMsg.id, callID: value.toolCallId, title: `⚠️ Potential doom loop detected: "${value.toolName}"`, metadata: { From 277955764698c76949c0735c059910430aa50814 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 29 Oct 2025 21:40:31 -0500 Subject: [PATCH 5/7] fix --- packages/opencode/src/session/prompt.ts | 28 ++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 7ad71b0502..df18e7afe9 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1048,6 +1048,20 @@ export namespace SessionPrompt { case "tool-call": { const match = toolcalls[value.toolCallId] if (match) { + const part = await Session.updatePart({ + ...match, + tool: value.toolName, + state: { + status: "running", + input: value.input, + time: { + start: Date.now(), + }, + }, + metadata: value.providerMetadata, + }) + toolcalls[value.toolCallId] = part as MessageV2.ToolPart + const currentParts = await Session.getParts(assistantMsg.id) const toolParts = currentParts.filter( (p): p is MessageV2.ToolPart => p.type === "tool" && p.state.status !== "pending", @@ -1075,20 +1089,6 @@ export namespace SessionPrompt { }, }) } - - const part = await Session.updatePart({ - ...match, - tool: value.toolName, - state: { - status: "running", - input: value.input, - time: { - start: Date.now(), - }, - }, - metadata: value.providerMetadata, - }) - toolcalls[value.toolCallId] = part as MessageV2.ToolPart } break } From 798f3463edb08f14341735e14978c40908f76a07 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 29 Oct 2025 22:13:41 -0500 Subject: [PATCH 6/7] wip --- packages/opencode/src/session/prompt.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index df18e7afe9..25ffa81710 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1062,15 +1062,13 @@ export namespace SessionPrompt { }) toolcalls[value.toolCallId] = part as MessageV2.ToolPart - const currentParts = await Session.getParts(assistantMsg.id) - const toolParts = currentParts.filter( - (p): p is MessageV2.ToolPart => p.type === "tool" && p.state.status !== "pending", - ) - const lastThree = toolParts.slice(-3) + const parts = await Session.getParts(assistantMsg.id) + const lastThree = parts.slice(-3) if ( lastThree.length === 3 && lastThree.every( (p) => + p.type === "tool" && p.tool === value.toolName && p.state.status !== "pending" && JSON.stringify(p.state.input) === JSON.stringify(value.input), From b96134dab884ffc234594003d60d6b9ea13faa61 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Thu, 30 Oct 2025 00:11:24 -0500 Subject: [PATCH 7/7] tweaks --- packages/opencode/src/session/prompt.ts | 11 ++++++----- packages/tui/internal/components/chat/message.go | 12 ++++++++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 25ffa81710..22957f9203 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -55,6 +55,7 @@ export namespace SessionPrompt { const log = Log.create({ service: "session.prompt" }) export const OUTPUT_TOKEN_MAX = 32_000 const MAX_RETRIES = 10 + const DOOM_LOOP_THRESHOLD = 3 export const Event = { Idle: Bus.event( @@ -1063,9 +1064,9 @@ export namespace SessionPrompt { toolcalls[value.toolCallId] = part as MessageV2.ToolPart const parts = await Session.getParts(assistantMsg.id) - const lastThree = parts.slice(-3) + const lastThree = parts.slice(-DOOM_LOOP_THRESHOLD) if ( - lastThree.length === 3 && + lastThree.length === DOOM_LOOP_THRESHOLD && lastThree.every( (p) => p.type === "tool" && @@ -1075,15 +1076,15 @@ export namespace SessionPrompt { ) ) { await Permission.ask({ - type: tool.name, + type: "doom-loop", + pattern: value.toolName, sessionID: assistantMsg.sessionID, messageID: assistantMsg.id, callID: value.toolCallId, - title: `⚠️ Potential doom loop detected: "${value.toolName}"`, + title: `Possible doom loop: "${value.toolName}" called ${DOOM_LOOP_THRESHOLD} times with identical arguments`, metadata: { tool: value.toolName, input: value.input, - reason: "doom-loop-detected", }, }) } diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go index fc5a21ad1e..801545a88f 100644 --- a/packages/tui/internal/components/chat/message.go +++ b/packages/tui/internal/components/chat/message.go @@ -504,7 +504,11 @@ func renderToolDetails( base := styles.NewStyle().Background(backgroundColor) text := base.Foreground(t.Text()).Bold(true).Render muted := base.Foreground(t.TextMuted()).Render - permissionContent = "Permission required to run this tool:\n\n" + if permission.Type == "doom-loop" { + permissionContent = permission.Title + "\n\n" + } else { + permissionContent = "Permission required to run this tool:\n\n" + } permissionContent += text( "enter ", ) + muted( @@ -642,9 +646,9 @@ func renderToolDetails( for _, item := range todos.([]any) { todo := item.(map[string]any) content := todo["content"] - if content == nil { - continue - } + if content == nil { + continue + } switch todo["status"] { case "completed": body += fmt.Sprintf("- [x] %s\n", content)