Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/opencode/src/session/prompt/text-loop-recovery.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
28 changes: 28 additions & 0 deletions packages/opencode/src/tool/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,34 @@ function wrap<Parameters extends z.ZodType, Result extends Metadata>(
...(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) => {
Expand Down
15 changes: 7 additions & 8 deletions packages/opencode/test/session/text-loop-detection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
})
})
Loading