From d983b9485d91306da6ca72d726348812840f2831 Mon Sep 17 00:00:00 2001 From: ElecTwix Date: Thu, 30 Oct 2025 08:13:18 +0300 Subject: [PATCH] fix: add doom loop detection (#3445) Co-authored-by: Aiden Cline --- packages/opencode/src/session/prompt.ts | 27 +++++++++++++++++++ .../tui/internal/components/chat/message.go | 12 ++++++--- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 43bb7370..bcab7c84 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -56,6 +56,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( @@ -1068,6 +1069,32 @@ export namespace SessionPrompt { metadata: value.providerMetadata, }) toolcalls[value.toolCallId] = part as MessageV2.ToolPart + + const parts = await Session.getParts(assistantMsg.id) + const lastThree = parts.slice(-DOOM_LOOP_THRESHOLD) + if ( + lastThree.length === DOOM_LOOP_THRESHOLD && + lastThree.every( + (p) => + p.type === "tool" && + p.tool === value.toolName && + p.state.status !== "pending" && + JSON.stringify(p.state.input) === JSON.stringify(value.input), + ) + ) { + await Permission.ask({ + type: "doom-loop", + pattern: value.toolName, + sessionID: assistantMsg.sessionID, + messageID: assistantMsg.id, + callID: value.toolCallId, + title: `Possible doom loop: "${value.toolName}" called ${DOOM_LOOP_THRESHOLD} times with identical arguments`, + metadata: { + tool: value.toolName, + input: value.input, + }, + }) + } } break } diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go index fc5a21ad..801545a8 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)