From 8e7abbede7ae1958e81af31286c90978744fc1bc Mon Sep 17 00:00:00 2001 From: Rohan Poudel Date: Sun, 21 Jun 2026 22:13:10 +0545 Subject: [PATCH] fix(loop-detection): add pre-execution tool dedup and reduce threshold to 2 (#1162) --- .../src/session/prompt/text-loop-recovery.ts | 2 +- packages/opencode/src/tool/tool.ts | 28 +++++++++++++++++++ .../test/session/text-loop-detection.test.ts | 15 +++++----- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/session/prompt/text-loop-recovery.ts b/packages/opencode/src/session/prompt/text-loop-recovery.ts index 6b96d6fac..22b6f48e7 100644 --- a/packages/opencode/src/session/prompt/text-loop-recovery.ts +++ b/packages/opencode/src/session/prompt/text-loop-recovery.ts @@ -1,5 +1,5 @@ export const TEXT_LOOP_BUFFER_SIZE = 5 -export const TEXT_LOOP_TRIGGER_COUNT = 3 +export const TEXT_LOOP_TRIGGER_COUNT = 2 export const TEXT_LOOP_MAX_RECOVERY = 2 export function normalizeForLoopDetection(text: string): string { diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index c2a406e8d..27fa06ac6 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -107,6 +107,34 @@ function wrap( ...(ctx.callID ? { "tool.call_id": ctx.callID } : {}), } return Effect.gen(function* () { + // Pre-execution dedup (Issue #1162) + let duplicatePart: any | undefined + for (let i = ctx.messages.length - 1; i >= 0; i--) { + const msg = ctx.messages[i] + if (msg.info.role === "user") break + const toolParts = msg.parts.filter( + (p: any) => + p.type === "tool" && + p.tool === id && + p.state && + p.state.status === "completed" && + JSON.stringify(p.state.input) === JSON.stringify(args) + ) + if (toolParts.length > 0) { + duplicatePart = toolParts[toolParts.length - 1] + break + } + } + + if (duplicatePart && duplicatePart.state && duplicatePart.state.status === "completed") { + return { + title: duplicatePart.state.title, + metadata: duplicatePart.state.metadata as Result, + output: duplicatePart.state.output, + attachments: duplicatePart.state.attachments, + } + } + yield* Effect.try({ try: () => toolInfo.parameters.parse(args), catch: (error) => { diff --git a/packages/opencode/test/session/text-loop-detection.test.ts b/packages/opencode/test/session/text-loop-detection.test.ts index ea1234d28..9b53f630d 100644 --- a/packages/opencode/test/session/text-loop-detection.test.ts +++ b/packages/opencode/test/session/text-loop-detection.test.ts @@ -72,12 +72,11 @@ describe("detectTextLoop", () => { }) describe("text loop detection integration logic", () => { - test("full detection flow: 3 identical triggers detection", () => { + test("full detection flow: 2 identical triggers detection", () => { const buffer: string[] = [] const texts = [ "Let me check if one was already created earlier and update it.", "Let me check if one was already created earlier and update it.", - "Let me check if one was already created earlier and update it.", ] let triggered = false @@ -126,8 +125,8 @@ describe("text loop detection integration logic", () => { let recoveryAttempts = 0 const repeatedText = "The user wants me to create a ChangeLog file." - // First 3 identical → trigger #1 - for (let i = 0; i < 3; i++) { + // First 2 identical → trigger #1 + for (let i = 0; i < 2; i++) { buffer.push(normalizeForLoopDetection(repeatedText)) } expect(detectTextLoop(buffer, TEXT_LOOP_TRIGGER_COUNT)).toBe(true) @@ -146,7 +145,7 @@ describe("text loop detection integration logic", () => { buffer.length = 0 // Third trigger would exceed max - for (let i = 0; i < 3; i++) { + for (let i = 0; i < 2; i++) { buffer.push(normalizeForLoopDetection(repeatedText)) } expect(detectTextLoop(buffer, TEXT_LOOP_TRIGGER_COUNT)).toBe(true) @@ -159,8 +158,8 @@ describe("text loop detection integration logic", () => { const repeatedText = "I am stuck in a loop" const differentText = "OK I will try something else" - // 3 identical → trigger - for (let i = 0; i < 3; i++) { + // 2 identical → trigger + for (let i = 0; i < 2; i++) { buffer.push(normalizeForLoopDetection(repeatedText)) } expect(detectTextLoop(buffer, TEXT_LOOP_TRIGGER_COUNT)).toBe(true) @@ -179,7 +178,7 @@ describe("text loop detection integration logic", () => { test("constants have expected values", () => { expect(TEXT_LOOP_BUFFER_SIZE).toBe(5) - expect(TEXT_LOOP_TRIGGER_COUNT).toBe(3) + expect(TEXT_LOOP_TRIGGER_COUNT).toBe(2) expect(TEXT_LOOP_MAX_RECOVERY).toBe(2) }) })