Skip to content

Commit b9fa2a3

Browse files
Mouguazi04
authored andcommitted
fix(background-agent): prevent circuit breaker false positives on flat-format events
1 parent 500784a commit b9fa2a3

File tree

3 files changed

+51
-18
lines changed

3 files changed

+51
-18
lines changed

src/features/background-agent/loop-detector.test.ts

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/// <reference types="bun-types" />
2+
13
import { describe, expect, test } from "bun:test"
24
import {
35
createToolCallSignature,
@@ -19,7 +21,7 @@ function buildWindow(
1921
}
2022

2123
function buildWindowWithInputs(
22-
calls: Array<{ tool: string; input?: Record<string, unknown> }>,
24+
calls: Array<{ tool: string; input?: Record<string, unknown> | null }>,
2325
override?: Parameters<typeof resolveCircuitBreakerSettings>[0]
2426
) {
2527
const settings = resolveCircuitBreakerSettings(override)
@@ -148,7 +150,12 @@ describe("loop-detector", () => {
148150

149151
describe("#given the same tool is called consecutively", () => {
150152
test("#when evaluated #then it triggers", () => {
151-
const window = buildWindow(Array.from({ length: 20 }, () => "read"))
153+
const window = buildWindowWithInputs(
154+
Array.from({ length: 20 }, () => ({
155+
tool: "read",
156+
input: { filePath: "/src/same.ts" },
157+
}))
158+
)
152159

153160
const result = detectRepetitiveToolUse(window)
154161

@@ -176,15 +183,25 @@ describe("loop-detector", () => {
176183

177184
describe("#given threshold boundary", () => {
178185
test("#when below threshold #then it does not trigger", () => {
179-
const belowThresholdWindow = buildWindow(Array.from({ length: 19 }, () => "read"))
186+
const belowThresholdWindow = buildWindowWithInputs(
187+
Array.from({ length: 19 }, () => ({
188+
tool: "read",
189+
input: { filePath: "/src/same.ts" },
190+
}))
191+
)
180192

181193
const result = detectRepetitiveToolUse(belowThresholdWindow)
182194

183195
expect(result).toEqual({ triggered: false })
184196
})
185197

186198
test("#when equal to threshold #then it triggers", () => {
187-
const atThresholdWindow = buildWindow(Array.from({ length: 20 }, () => "read"))
199+
const atThresholdWindow = buildWindowWithInputs(
200+
Array.from({ length: 20 }, () => ({
201+
tool: "read",
202+
input: { filePath: "/src/same.ts" },
203+
}))
204+
)
188205

189206
const result = detectRepetitiveToolUse(atThresholdWindow)
190207

@@ -224,16 +241,22 @@ describe("loop-detector", () => {
224241
})
225242
})
226243

227-
describe("#given tool calls with no input", () => {
228-
test("#when evaluated #then it triggers", () => {
244+
describe("#given tool calls with undefined input", () => {
245+
test("#when evaluated #then it does not trigger", () => {
229246
const calls = Array.from({ length: 20 }, () => ({ tool: "read" }))
230247
const window = buildWindowWithInputs(calls)
231248
const result = detectRepetitiveToolUse(window)
232-
expect(result).toEqual({
233-
triggered: true,
234-
toolName: "read",
235-
repeatedCount: 20,
236-
})
249+
expect(result).toEqual({ triggered: false })
250+
})
251+
})
252+
253+
describe("#given tool calls with null input", () => {
254+
test("#when evaluated #then it does not trigger", () => {
255+
const calls = Array.from({ length: 20 }, () => ({ tool: "read", input: null }))
256+
const window = buildWindowWithInputs(calls)
257+
const result = detectRepetitiveToolUse(window)
258+
259+
expect(result).toEqual({ triggered: false })
237260
})
238261
})
239262
})

src/features/background-agent/loop-detector.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ export function recordToolCall(
3636
settings: CircuitBreakerSettings,
3737
toolInput?: Record<string, unknown> | null
3838
): ToolCallWindow {
39+
if (toolInput === undefined || toolInput === null) {
40+
return {
41+
lastSignature: `${toolName}::__unknown-input__`,
42+
consecutiveCount: 1,
43+
threshold: settings.consecutiveThreshold,
44+
}
45+
}
46+
3947
const signature = createToolCallSignature(toolName, toolInput)
4048

4149
if (window && window.lastSignature === signature) {

src/features/background-agent/manager-circuit-breaker.test.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/// <reference types="bun-types" />
2+
13
import { describe, expect, test } from "bun:test"
24
import type { PluginInput } from "@opencode-ai/plugin"
35
import { tmpdir } from "node:os"
@@ -38,8 +40,8 @@ async function flushAsyncWork() {
3840
}
3941

4042
describe("BackgroundManager circuit breaker", () => {
41-
describe("#given the same tool is called consecutively", () => {
42-
test("#when consecutive tool events arrive #then the task is cancelled", async () => {
43+
describe("#given flat-format tool events have no state.input", () => {
44+
test("#when 20 consecutive read events arrive #then the task keeps running", async () => {
4345
const manager = createManager({
4446
circuitBreaker: {
4547
consecutiveThreshold: 20,
@@ -71,8 +73,8 @@ describe("BackgroundManager circuit breaker", () => {
7173

7274
await flushAsyncWork()
7375

74-
expect(task.status).toBe("cancelled")
75-
expect(task.error).toContain("read 20 consecutive times")
76+
expect(task.status).toBe("running")
77+
expect(task.progress?.toolCalls).toBe(20)
7678
})
7779
})
7880

@@ -126,7 +128,7 @@ describe("BackgroundManager circuit breaker", () => {
126128
})
127129

128130
describe("#given the absolute cap is configured lower than the repetition detector needs", () => {
129-
test("#when the raw tool-call cap is reached #then the backstop still cancels the task", async () => {
131+
test("#when repeated flat-format tool events reach maxToolCalls #then the backstop still cancels the task", async () => {
130132
const manager = createManager({
131133
maxToolCalls: 3,
132134
circuitBreaker: {
@@ -150,10 +152,10 @@ describe("BackgroundManager circuit breaker", () => {
150152
}
151153
getTaskMap(manager).set(task.id, task)
152154

153-
for (const toolName of ["read", "grep", "edit"]) {
155+
for (let i = 0; i < 3; i++) {
154156
manager.handleEvent({
155157
type: "message.part.updated",
156-
properties: { sessionID: task.sessionID, type: "tool", tool: toolName },
158+
properties: { sessionID: task.sessionID, type: "tool", tool: "read" },
157159
})
158160
}
159161

0 commit comments

Comments
 (0)