From 1876a848af175753c1721645bbe9d63a1df6d1db Mon Sep 17 00:00:00 2001 From: Sandy Tao Date: Fri, 30 Jan 2026 16:55:53 -0800 Subject: [PATCH] feat(core): add setting to disable loop detection --- docs/cli/settings.md | 1 + docs/get-started/configuration.md | 6 ++++++ packages/cli/src/config/config.ts | 1 + packages/cli/src/config/settingsSchema.ts | 10 ++++++++++ packages/cli/src/ui/hooks/useGeminiStream.test.tsx | 1 + packages/core/src/config/config.ts | 7 +++++++ .../core/src/services/loopDetectionService.test.ts | 11 +++++++++++ packages/core/src/services/loopDetectionService.ts | 4 ++-- schemas/settings.schema.json | 7 +++++++ 9 files changed, 46 insertions(+), 2 deletions(-) diff --git a/docs/cli/settings.md b/docs/cli/settings.md index ab637aed3e..a380d53558 100644 --- a/docs/cli/settings.md +++ b/docs/cli/settings.md @@ -77,6 +77,7 @@ they appear in the UI. | ----------------------- | ---------------------------- | -------------------------------------------------------------------------------------- | ------- | | Max Session Turns | `model.maxSessionTurns` | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` | | Compression Threshold | `model.compressionThreshold` | The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3). | `0.5` | +| Disable Loop Detection | `model.disableLoopDetection` | Disable automatic detection and prevention of infinite loops. | `false` | | Skip Next Speaker Check | `model.skipNextSpeakerCheck` | Skip the next speaker check. | `true` | ### Context diff --git a/docs/get-started/configuration.md b/docs/get-started/configuration.md index 5ce8231a51..31d109d426 100644 --- a/docs/get-started/configuration.md +++ b/docs/get-started/configuration.md @@ -326,6 +326,12 @@ their corresponding top-level category object in your `settings.json` file. - **Default:** `0.5` - **Requires restart:** Yes +- **`model.disableLoopDetection`** (boolean): + - **Description:** Disable automatic detection and prevention of infinite + loops. + - **Default:** `false` + - **Requires restart:** Yes + - **`model.skipNextSpeakerCheck`** (boolean): - **Description:** Skip the next speaker check. - **Default:** `true` diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 0c5063faee..c8e1e8c975 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -762,6 +762,7 @@ export async function loadCliConfig( noBrowser: !!process.env['NO_BROWSER'], summarizeToolOutput: settings.model?.summarizeToolOutput, ideMode, + disableLoopDetection: settings.model?.disableLoopDetection, compressionThreshold: settings.model?.compressionThreshold, folderTrust, interactive, diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index a34163ccb3..c08d31fa47 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -739,6 +739,16 @@ const SETTINGS_SCHEMA = { 'The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3).', showInDialog: true, }, + disableLoopDetection: { + type: 'boolean', + label: 'Disable Loop Detection', + category: 'Model', + requiresRestart: true, + default: false, + description: + 'Disable automatic detection and prevention of infinite loops.', + showInDialog: true, + }, skipNextSpeakerCheck: { type: 'boolean', label: 'Skip Next Speaker Check', diff --git a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx index 1c4434a34a..5b94bc0812 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx +++ b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx @@ -253,6 +253,7 @@ describe('useGeminiStream', () => { getUserMemory: vi.fn(() => ''), getIdeMode: vi.fn(() => false), getEnableHooks: vi.fn(() => false), + getDisableLoopDetection: vi.fn(() => false), } as unknown as Config; beforeEach(() => { diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 0f027c989c..841c6b2d45 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -393,6 +393,7 @@ export interface ConfigParameters { includeDirectories?: string[]; bugCommand?: BugCommandSettings; model: string; + disableLoopDetection?: boolean; maxSessionTurns?: number; experimentalZedIntegration?: boolean; listSessions?: boolean; @@ -531,6 +532,7 @@ export class Config { private readonly cwd: string; private readonly bugCommand: BugCommandSettings | undefined; private model: string; + private readonly disableLoopDetection: boolean; private previewFeatures: boolean | undefined; private hasAccessToPreviewModel: boolean = false; private readonly noBrowser: boolean; @@ -697,6 +699,7 @@ export class Config { this.fileDiscoveryService = params.fileDiscoveryService ?? null; this.bugCommand = params.bugCommand; this.model = params.model; + this.disableLoopDetection = params.disableLoopDetection ?? false; this._activeModel = params.model; this.enableAgents = params.enableAgents ?? false; this.agents = params.agents ?? {}; @@ -1118,6 +1121,10 @@ export class Config { return this.model; } + getDisableLoopDetection(): boolean { + return this.disableLoopDetection; + } + setModel(newModel: string, isTemporary: boolean = true): void { if (this.model !== newModel || this._activeModel !== newModel) { this.model = newModel; diff --git a/packages/core/src/services/loopDetectionService.test.ts b/packages/core/src/services/loopDetectionService.test.ts index 660f6c48e8..59862e0a4a 100644 --- a/packages/core/src/services/loopDetectionService.test.ts +++ b/packages/core/src/services/loopDetectionService.test.ts @@ -38,6 +38,7 @@ describe('LoopDetectionService', () => { mockConfig = { getTelemetryEnabled: () => true, isInteractive: () => false, + getDisableLoopDetection: () => false, getModelAvailabilityService: vi .fn() .mockReturnValue(createAvailabilityServiceMock()), @@ -162,6 +163,15 @@ describe('LoopDetectionService', () => { // Should now return false even though a loop was previously detected expect(service.addAndCheck(event)).toBe(false); }); + + it('should skip loop detection if disabled in config', () => { + vi.spyOn(mockConfig, 'getDisableLoopDetection').mockReturnValue(true); + const event = createToolCallRequestEvent('testTool', { param: 'value' }); + for (let i = 0; i < TOOL_CALL_LOOP_THRESHOLD + 2; i++) { + expect(service.addAndCheck(event)).toBe(false); + } + expect(loggers.logLoopDetected).not.toHaveBeenCalled(); + }); }); describe('Content Loop Detection', () => { @@ -742,6 +752,7 @@ describe('LoopDetectionService LLM Checks', () => { mockConfig = { getGeminiClient: () => mockGeminiClient, getBaseLlmClient: () => mockBaseLlmClient, + getDisableLoopDetection: () => false, getDebugMode: () => false, getTelemetryEnabled: () => true, getModel: vi.fn().mockReturnValue('cognitive-loop-v1'), diff --git a/packages/core/src/services/loopDetectionService.ts b/packages/core/src/services/loopDetectionService.ts index d8049dbdb6..378b0faaa3 100644 --- a/packages/core/src/services/loopDetectionService.ts +++ b/packages/core/src/services/loopDetectionService.ts @@ -147,7 +147,7 @@ export class LoopDetectionService { * @returns true if a loop is detected, false otherwise */ addAndCheck(event: ServerGeminiStreamEvent): boolean { - if (this.disabledForSession) { + if (this.disabledForSession || this.config.getDisableLoopDetection()) { return false; } @@ -182,7 +182,7 @@ export class LoopDetectionService { * @returns A promise that resolves to `true` if a loop is detected, and `false` otherwise. */ async turnStarted(signal: AbortSignal) { - if (this.disabledForSession) { + if (this.disabledForSession || this.config.getDisableLoopDetection()) { return false; } this.turnsInCurrentPrompt++; diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index d33c75bf63..bb85f5a397 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -457,6 +457,13 @@ "default": 0.5, "type": "number" }, + "disableLoopDetection": { + "title": "Disable Loop Detection", + "description": "Disable automatic detection and prevention of infinite loops.", + "markdownDescription": "Disable automatic detection and prevention of infinite loops.\n\n- Category: `Model`\n- Requires restart: `yes`\n- Default: `false`", + "default": false, + "type": "boolean" + }, "skipNextSpeakerCheck": { "title": "Skip Next Speaker Check", "description": "Skip the next speaker check.",