From c8213c970e20c20f045e324a7e470a271de80cca Mon Sep 17 00:00:00 2001 From: tad-hq <213478119+tad-hq@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:29:06 -0600 Subject: [PATCH] fix(circuit-breaker): add enabled config flag as escape hatch --- src/config/schema/background-task.ts | 1 + src/features/background-agent/constants.ts | 1 + .../background-agent/loop-detector.test.ts | 45 +++++++++++++++++++ .../background-agent/loop-detector.ts | 3 ++ 4 files changed, 50 insertions(+) diff --git a/src/config/schema/background-task.ts b/src/config/schema/background-task.ts index 5bec5065e..3e694d53e 100644 --- a/src/config/schema/background-task.ts +++ b/src/config/schema/background-task.ts @@ -1,6 +1,7 @@ import { z } from "zod" const CircuitBreakerConfigSchema = z.object({ + enabled: z.boolean().optional(), maxToolCalls: z.number().int().min(10).optional(), windowSize: z.number().int().min(5).optional(), repetitionThresholdPercent: z.number().gt(0).max(100).optional(), diff --git a/src/features/background-agent/constants.ts b/src/features/background-agent/constants.ts index 7f5b1492d..c2b3718e9 100644 --- a/src/features/background-agent/constants.ts +++ b/src/features/background-agent/constants.ts @@ -9,6 +9,7 @@ export const DEFAULT_MESSAGE_STALENESS_TIMEOUT_MS = 1_800_000 export const DEFAULT_MAX_TOOL_CALLS = 200 export const DEFAULT_CIRCUIT_BREAKER_WINDOW_SIZE = 20 export const DEFAULT_CIRCUIT_BREAKER_REPETITION_THRESHOLD_PERCENT = 80 +export const DEFAULT_CIRCUIT_BREAKER_ENABLED = true export const MIN_RUNTIME_BEFORE_STALE_MS = 30_000 export const MIN_IDLE_TIME_MS = 5000 export const POLLING_INTERVAL_MS = 3000 diff --git a/src/features/background-agent/loop-detector.test.ts b/src/features/background-agent/loop-detector.test.ts index b3d5de806..a1355f3c9 100644 --- a/src/features/background-agent/loop-detector.test.ts +++ b/src/features/background-agent/loop-detector.test.ts @@ -31,12 +31,57 @@ describe("loop-detector", () => { }) expect(result).toEqual({ + enabled: true, maxToolCalls: 120, windowSize: 10, repetitionThresholdPercent: 70, }) }) }) + + describe("#given no enabled config", () => { + test("#when resolved #then enabled defaults to true", () => { + const result = resolveCircuitBreakerSettings({ + circuitBreaker: { + maxToolCalls: 100, + windowSize: 5, + repetitionThresholdPercent: 60, + }, + }) + + expect(result.enabled).toBe(true) + }) + }) + + describe("#given enabled is false in config", () => { + test("#when resolved #then enabled is false", () => { + const result = resolveCircuitBreakerSettings({ + circuitBreaker: { + enabled: false, + maxToolCalls: 100, + windowSize: 5, + repetitionThresholdPercent: 60, + }, + }) + + expect(result.enabled).toBe(false) + }) + }) + + describe("#given enabled is true in config", () => { + test("#when resolved #then enabled is true", () => { + const result = resolveCircuitBreakerSettings({ + circuitBreaker: { + enabled: true, + maxToolCalls: 100, + windowSize: 5, + repetitionThresholdPercent: 60, + }, + }) + + expect(result.enabled).toBe(true) + }) + }) }) describe("detectRepetitiveToolUse", () => { diff --git a/src/features/background-agent/loop-detector.ts b/src/features/background-agent/loop-detector.ts index 610ddf147..a41597825 100644 --- a/src/features/background-agent/loop-detector.ts +++ b/src/features/background-agent/loop-detector.ts @@ -1,5 +1,6 @@ import type { BackgroundTaskConfig } from "../../config/schema" import { + DEFAULT_CIRCUIT_BREAKER_ENABLED, DEFAULT_CIRCUIT_BREAKER_REPETITION_THRESHOLD_PERCENT, DEFAULT_CIRCUIT_BREAKER_WINDOW_SIZE, DEFAULT_MAX_TOOL_CALLS, @@ -7,6 +8,7 @@ import { import type { ToolCallWindow } from "./types" export interface CircuitBreakerSettings { + enabled: boolean maxToolCalls: number windowSize: number repetitionThresholdPercent: number @@ -24,6 +26,7 @@ export function resolveCircuitBreakerSettings( config?: BackgroundTaskConfig ): CircuitBreakerSettings { return { + enabled: config?.circuitBreaker?.enabled ?? DEFAULT_CIRCUIT_BREAKER_ENABLED, maxToolCalls: config?.circuitBreaker?.maxToolCalls ?? config?.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS, windowSize: config?.circuitBreaker?.windowSize ?? DEFAULT_CIRCUIT_BREAKER_WINDOW_SIZE,