diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 6adeb6f7e0..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( @@ -1061,6 +1062,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 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)